Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

# Local agent instructions
AGENTS.md
CLAUDE.MD
CLAUDE.md



Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ https://github.com/user-attachments/assets/4b5ed159-8d1c-44cb-8fe4-e0f2ea41d818
- Selection: Alt-drag, <kbd>V</kbd> tool, properties panel
- Duplicate (<kbd>Ctrl+D</kbd>), delete (<kbd>Delete</kbd>), undo/redo
- Color picker, palettes, size via hotkeys or scroll
- Radial menu at cursor (<kbd>Middle-click</kbd>): quick tool/color selection + scroll size adjust

### Boards
- Named boards with transparent overlay or custom backgrounds
Expand Down Expand Up @@ -530,6 +531,7 @@ Press <kbd>F1</kbd> for the complete in-app cheat sheet.
| Quick reference | <kbd>Shift+F1</kbd> |
| Configurator | <kbd>F11</kbd> |
| Command palette | <kbd>Ctrl+K</kbd> |
| Radial menu | <kbd>Middle-click</kbd> (idle) open/close; <kbd>Left-click</kbd> select; <kbd>Right-click</kbd>/<kbd>Escape</kbd> dismiss; scroll adjusts active tool size |
| Status bar | <kbd>F4</kbd> / <kbd>F12</kbd> |
| Apply preset slot | <kbd>1</kbd> - <kbd>5</kbd> |
| Save preset slot | <kbd>Shift+1</kbd> - <kbd>Shift+5</kbd> |
Expand Down
7 changes: 7 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ toggle_presenter_mode = ["Ctrl+Shift+M"]
# Toggle fill for rectangle/ellipse
toggle_fill = []

# Optional keyboard binding to toggle radial menu at cursor
toggle_radial_menu = []

# Toggle selection properties panel
toggle_selection_properties = ["Ctrl+Alt+P"]

Expand Down Expand Up @@ -259,6 +262,10 @@ active_output_badge = true
# Request fullscreen for the GNOME fallback overlay. Disable if fullscreen appears opaque.
#xdg_fullscreen = false

# Mouse button that toggles radial menu
# Options: "middle", "right", "disabled"
radial_menu_mouse_binding = "middle"

# ───────────────────────────────────────────────────────────────────────────────
# Floating Toolbars (Press F2/F9 to toggle)
# ───────────────────────────────────────────────────────────────────────────────
Expand Down
9 changes: 9 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ active_output_badge = true
# Request fullscreen for the GNOME fallback overlay (disable if opaque)
#xdg_fullscreen = false

# Mouse button that toggles radial menu
# Options: "middle", "right", "disabled"
radial_menu_mouse_binding = "middle"

# Status bar styling
[ui.status_bar_style]
font_size = 21.0
Expand Down Expand Up @@ -309,6 +313,7 @@ enabled = true
- **Context menu**: `ui.context_menu.enabled` toggles right-click / keyboard menus
- **Output focus**: `multi_monitor_enabled` controls output-cycling shortcuts; `active_output_badge` shows the current monitor in the status bar
- **GNOME fallback**: `preferred_output` pins the xdg-shell overlay to a specific monitor; `xdg_fullscreen` requests fullscreen instead of maximized
- **Radial menu trigger**: `radial_menu_mouse_binding` selects which mouse button opens radial menu (`middle` default, `right`, or `disabled`)

**Multi-monitor behavior:**
- Use `focus_prev_output` / `focus_next_output` (default: <kbd>Ctrl+Alt+Shift+←</kbd>/<kbd>Ctrl+Alt+Shift+→</kbd>) to move overlay focus between outputs.
Expand All @@ -321,6 +326,7 @@ enabled = true
- Show status bar: true
- Show frozen badge: false
- Position: bottom-left
- Radial menu mouse trigger: middle
- Status bar font: 21px
- Help overlay font: 14px
- Semi-transparent dark backgrounds with muted borders
Expand Down Expand Up @@ -799,6 +805,9 @@ toggle_click_highlight = ["Ctrl+Shift+H"]
# Toggle fill for rectangle/ellipse
toggle_fill = []

# Optional keyboard binding to toggle radial menu at cursor
toggle_radial_menu = []

# Toggle selection properties panel
toggle_selection_properties = ["Ctrl+Alt+P"]

Expand Down
1 change: 1 addition & 0 deletions src/backend/wayland/backend/state_init/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub(super) fn build_input_state(config: &Config) -> InputState {
input_state.show_floating_badge_always = config.ui.show_floating_badge_always;
input_state.show_active_output_badge = config.ui.active_output_badge;
input_state.command_palette_toast_duration_ms = config.ui.command_palette_toast_duration_ms;
input_state.radial_menu_mouse_binding = config.ui.radial_menu_mouse_binding;
#[cfg(tablet)]
{
input_state.pressure_variation_threshold = config.tablet.pressure_variation_threshold;
Expand Down
88 changes: 54 additions & 34 deletions src/backend/wayland/handlers/pointer/axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ impl WaylandState {
} else {
0
};
// Handle radial menu scroll-to-thickness
if self.input_state.is_radial_menu_open() {
if scroll_direction != 0 {
let delta = if scroll_direction > 0 { -1.0 } else { 1.0 };
self.adjust_active_tool_thickness(delta, true);
}
return;
}

// Handle command palette scrolling
if self.input_state.command_palette_open {
if scroll_direction != 0 {
Expand Down Expand Up @@ -114,42 +123,53 @@ impl WaylandState {
}
std::cmp::Ordering::Greater | std::cmp::Ordering::Less => {
let delta = if scroll_direction > 0 { -1.0 } else { 1.0 };
let eraser_active = self.input_state.active_tool() == Tool::Eraser;
#[cfg(tablet)]
let prev_thickness = self.input_state.current_thickness;

if self.input_state.nudge_thickness_for_active_tool(delta) {
if eraser_active {
debug!(
"Eraser size adjusted: {:.0}px",
self.input_state.eraser_size
);
} else {
debug!(
"Thickness adjusted: {:.0}px",
self.input_state.current_thickness
);
}
self.input_state.needs_redraw = true;
if !eraser_active {
self.save_drawing_preferences();
}
}
#[cfg(tablet)]
if !eraser_active
&& (self.input_state.current_thickness - prev_thickness).abs() > f64::EPSILON
{
self.stylus_base_thickness = Some(self.input_state.current_thickness);
if self.stylus_tip_down {
self.stylus_pressure_thickness = Some(self.input_state.current_thickness);
self.record_stylus_peak(self.input_state.current_thickness);
} else {
self.stylus_pressure_thickness = None;
self.stylus_peak_thickness = None;
}
}
self.adjust_active_tool_thickness(delta, false);
}
std::cmp::Ordering::Equal => {}
}
}

fn adjust_active_tool_thickness(&mut self, delta: f64, radial_menu_path: bool) {
let eraser_active = self.input_state.active_tool() == Tool::Eraser;
#[cfg(tablet)]
let prev_thickness = self.input_state.current_thickness;

let changed = if radial_menu_path {
self.input_state.radial_menu_adjust_thickness(delta)
} else if self.input_state.nudge_thickness_for_active_tool(delta) {
self.input_state.needs_redraw = true;
true
} else {
false
};

if changed {
if eraser_active {
debug!(
"Eraser size adjusted: {:.0}px",
self.input_state.eraser_size
);
} else {
debug!(
"Thickness adjusted: {:.0}px",
self.input_state.current_thickness
);
self.save_drawing_preferences();
}
}

#[cfg(tablet)]
if !eraser_active
&& (self.input_state.current_thickness - prev_thickness).abs() > f64::EPSILON
{
self.stylus_base_thickness = Some(self.input_state.current_thickness);
if self.stylus_tip_down {
self.stylus_pressure_thickness = Some(self.input_state.current_thickness);
self.record_stylus_peak(self.input_state.current_thickness);
} else {
self.stylus_pressure_thickness = None;
self.stylus_peak_thickness = None;
}
}
}
}
7 changes: 7 additions & 0 deletions src/backend/wayland/state/render/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ impl WaylandState {
self.input_state.clear_color_picker_popup_layout();
}

if self.input_state.is_radial_menu_open() {
self.input_state.update_radial_menu_layout(width, height);
crate::ui::render_radial_menu(ctx, &self.input_state, width, height);
} else {
self.input_state.clear_radial_menu_layout();
}

self.input_state.ui_toast_bounds =
crate::ui::render_ui_toast(ctx, &self.input_state, width, height);
crate::ui::render_preset_toast(ctx, &self.input_state, width, height);
Expand Down
11 changes: 11 additions & 0 deletions src/config/action_meta/entries/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ pub const ENTRIES: &[ActionMeta] = &[
true,
false
),
meta!(
ToggleRadialMenu,
"Radial Menu",
None,
"Toggle radial menu at cursor",
UI,
true,
false,
false,
&["pie menu"]
),
meta!(
ToggleSelectionProperties,
"Selection Properties",
Expand Down
12 changes: 12 additions & 0 deletions src/config/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ pub enum StatusPosition {
BottomRight,
}

/// Mouse button used to toggle the radial menu.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum RadialMenuMouseBinding {
/// Toggle radial menu with middle click.
Middle,
/// Toggle radial menu with right click.
Right,
/// Disable mouse-button toggling (keyboard action only).
Disabled,
}

/// Color specification - either a named color or RGB values.
///
/// # Examples
Expand Down
1 change: 1 addition & 0 deletions src/config/keybindings/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ pub enum Action {
TogglePresenterMode,
ToggleHighlightTool,
ToggleFill,
ToggleRadialMenu,
ToggleSelectionProperties,
OpenContextMenu,

Expand Down
1 change: 1 addition & 0 deletions src/config/keybindings/config/map/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl KeybindingsConfig {
inserter.insert_all(&self.ui.toggle_toolbar, Action::ToggleToolbar)?;
inserter.insert_all(&self.ui.toggle_presenter_mode, Action::TogglePresenterMode)?;
inserter.insert_all(&self.ui.toggle_fill, Action::ToggleFill)?;
inserter.insert_all(&self.ui.toggle_radial_menu, Action::ToggleRadialMenu)?;
inserter.insert_all(
&self.ui.toggle_selection_properties,
Action::ToggleSelectionProperties,
Expand Down
4 changes: 4 additions & 0 deletions src/config/keybindings/config/types/bindings/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub struct UiKeybindingsConfig {
#[serde(default = "default_toggle_fill")]
pub toggle_fill: Vec<String>,

#[serde(default = "default_toggle_radial_menu")]
pub toggle_radial_menu: Vec<String>,

#[serde(default = "default_toggle_selection_properties")]
pub toggle_selection_properties: Vec<String>,

Expand All @@ -49,6 +52,7 @@ impl Default for UiKeybindingsConfig {
toggle_toolbar: default_toggle_toolbar(),
toggle_presenter_mode: default_toggle_presenter_mode(),
toggle_fill: default_toggle_fill(),
toggle_radial_menu: default_toggle_radial_menu(),
toggle_selection_properties: default_toggle_selection_properties(),
open_context_menu: default_open_context_menu(),
open_configurator: default_open_configurator(),
Expand Down
4 changes: 4 additions & 0 deletions src/config/keybindings/defaults/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub(crate) fn default_toggle_fill() -> Vec<String> {
Vec::new()
}

pub(crate) fn default_toggle_radial_menu() -> Vec<String> {
Vec::new()
}

pub(crate) fn default_toggle_selection_properties() -> Vec<String> {
vec!["Ctrl+Alt+P".to_string()]
}
Expand Down
2 changes: 1 addition & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub use action_meta::{
action_meta, action_meta_iter, action_short_label,
};
pub use core::Config;
pub use enums::StatusPosition;
pub use enums::{RadialMenuMouseBinding, StatusPosition};
#[allow(unused_imports)]
pub use io::{ConfigSource, LoadedConfig};
pub use keybindings::{Action, KeyBinding, KeybindingsConfig};
Expand Down
11 changes: 10 additions & 1 deletion src/config/types/ui.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::config::enums::StatusPosition;
use crate::config::enums::{RadialMenuMouseBinding, StatusPosition};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -76,6 +76,10 @@ pub struct UiConfig {
#[serde(default = "default_xdg_fullscreen")]
pub xdg_fullscreen: bool,

/// Mouse button used to toggle the radial menu.
#[serde(default = "default_radial_menu_mouse_binding")]
pub radial_menu_mouse_binding: RadialMenuMouseBinding,

/// Click highlight visual indicator settings
#[serde(default)]
pub click_highlight: ClickHighlightConfig,
Expand Down Expand Up @@ -106,6 +110,7 @@ impl Default for UiConfig {
active_output_badge: default_active_output_badge(),
command_palette_toast_duration_ms: default_command_palette_toast_duration_ms(),
xdg_fullscreen: default_xdg_fullscreen(),
radial_menu_mouse_binding: default_radial_menu_mouse_binding(),
click_highlight: ClickHighlightConfig::default(),
context_menu: ContextMenuUiConfig::default(),
toolbar: ToolbarConfig::default(),
Expand Down Expand Up @@ -156,3 +161,7 @@ fn default_active_output_badge() -> bool {
fn default_status_position() -> StatusPosition {
StatusPosition::BottomLeft
}

fn default_radial_menu_mouse_binding() -> RadialMenuMouseBinding {
RadialMenuMouseBinding::Middle
}
Loading