Skip to content

Commit be247e2

Browse files
authored
Merge pull request #145 from devmobasa/feat/radial-menu
Add Radial menu for quick tool/color/thickness access
2 parents 5a3c085 + dd0fff9 commit be247e2

55 files changed

Lines changed: 1628 additions & 198 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
# Local agent instructions
1717
AGENTS.md
18-
CLAUDE.MD
18+
CLAUDE.md
1919

2020

2121

README.md

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

9394
### Boards
9495
- Named boards with transparent overlay or custom backgrounds
@@ -530,6 +531,7 @@ Press <kbd>F1</kbd> for the complete in-app cheat sheet.
530531
| Quick reference | <kbd>Shift+F1</kbd> |
531532
| Configurator | <kbd>F11</kbd> |
532533
| Command palette | <kbd>Ctrl+K</kbd> |
534+
| 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 |
533535
| Status bar | <kbd>F4</kbd> / <kbd>F12</kbd> |
534536
| Apply preset slot | <kbd>1</kbd> - <kbd>5</kbd> |
535537
| Save preset slot | <kbd>Shift+1</kbd> - <kbd>Shift+5</kbd> |

config.example.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ toggle_presenter_mode = ["Ctrl+Shift+M"]
132132
# Toggle fill for rectangle/ellipse
133133
toggle_fill = []
134134

135+
# Optional keyboard binding to toggle radial menu at cursor
136+
toggle_radial_menu = []
137+
135138
# Toggle selection properties panel
136139
toggle_selection_properties = ["Ctrl+Alt+P"]
137140

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

265+
# Mouse button that toggles radial menu
266+
# Options: "middle", "right", "disabled"
267+
radial_menu_mouse_binding = "middle"
268+
262269
# ───────────────────────────────────────────────────────────────────────────────
263270
# Floating Toolbars (Press F2/F9 to toggle)
264271
# ───────────────────────────────────────────────────────────────────────────────

docs/CONFIG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ active_output_badge = true
254254
# Request fullscreen for the GNOME fallback overlay (disable if opaque)
255255
#xdg_fullscreen = false
256256

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

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

808+
# Optional keyboard binding to toggle radial menu at cursor
809+
toggle_radial_menu = []
810+
802811
# Toggle selection properties panel
803812
toggle_selection_properties = ["Ctrl+Alt+P"]
804813

src/backend/wayland/backend/state_init/input_state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub(super) fn build_input_state(config: &Config) -> InputState {
5353
input_state.show_floating_badge_always = config.ui.show_floating_badge_always;
5454
input_state.show_active_output_badge = config.ui.active_output_badge;
5555
input_state.command_palette_toast_duration_ms = config.ui.command_palette_toast_duration_ms;
56+
input_state.radial_menu_mouse_binding = config.ui.radial_menu_mouse_binding;
5657
#[cfg(tablet)]
5758
{
5859
input_state.pressure_variation_threshold = config.tablet.pressure_variation_threshold;

src/backend/wayland/handlers/pointer/axis.rs

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ impl WaylandState {
2020
} else {
2121
0
2222
};
23+
// Handle radial menu scroll-to-thickness
24+
if self.input_state.is_radial_menu_open() {
25+
if scroll_direction != 0 {
26+
let delta = if scroll_direction > 0 { -1.0 } else { 1.0 };
27+
self.adjust_active_tool_thickness(delta, true);
28+
}
29+
return;
30+
}
31+
2332
// Handle command palette scrolling
2433
if self.input_state.command_palette_open {
2534
if scroll_direction != 0 {
@@ -114,42 +123,53 @@ impl WaylandState {
114123
}
115124
std::cmp::Ordering::Greater | std::cmp::Ordering::Less => {
116125
let delta = if scroll_direction > 0 { -1.0 } else { 1.0 };
117-
let eraser_active = self.input_state.active_tool() == Tool::Eraser;
118-
#[cfg(tablet)]
119-
let prev_thickness = self.input_state.current_thickness;
120-
121-
if self.input_state.nudge_thickness_for_active_tool(delta) {
122-
if eraser_active {
123-
debug!(
124-
"Eraser size adjusted: {:.0}px",
125-
self.input_state.eraser_size
126-
);
127-
} else {
128-
debug!(
129-
"Thickness adjusted: {:.0}px",
130-
self.input_state.current_thickness
131-
);
132-
}
133-
self.input_state.needs_redraw = true;
134-
if !eraser_active {
135-
self.save_drawing_preferences();
136-
}
137-
}
138-
#[cfg(tablet)]
139-
if !eraser_active
140-
&& (self.input_state.current_thickness - prev_thickness).abs() > f64::EPSILON
141-
{
142-
self.stylus_base_thickness = Some(self.input_state.current_thickness);
143-
if self.stylus_tip_down {
144-
self.stylus_pressure_thickness = Some(self.input_state.current_thickness);
145-
self.record_stylus_peak(self.input_state.current_thickness);
146-
} else {
147-
self.stylus_pressure_thickness = None;
148-
self.stylus_peak_thickness = None;
149-
}
150-
}
126+
self.adjust_active_tool_thickness(delta, false);
151127
}
152128
std::cmp::Ordering::Equal => {}
153129
}
154130
}
131+
132+
fn adjust_active_tool_thickness(&mut self, delta: f64, radial_menu_path: bool) {
133+
let eraser_active = self.input_state.active_tool() == Tool::Eraser;
134+
#[cfg(tablet)]
135+
let prev_thickness = self.input_state.current_thickness;
136+
137+
let changed = if radial_menu_path {
138+
self.input_state.radial_menu_adjust_thickness(delta)
139+
} else if self.input_state.nudge_thickness_for_active_tool(delta) {
140+
self.input_state.needs_redraw = true;
141+
true
142+
} else {
143+
false
144+
};
145+
146+
if changed {
147+
if eraser_active {
148+
debug!(
149+
"Eraser size adjusted: {:.0}px",
150+
self.input_state.eraser_size
151+
);
152+
} else {
153+
debug!(
154+
"Thickness adjusted: {:.0}px",
155+
self.input_state.current_thickness
156+
);
157+
self.save_drawing_preferences();
158+
}
159+
}
160+
161+
#[cfg(tablet)]
162+
if !eraser_active
163+
&& (self.input_state.current_thickness - prev_thickness).abs() > f64::EPSILON
164+
{
165+
self.stylus_base_thickness = Some(self.input_state.current_thickness);
166+
if self.stylus_tip_down {
167+
self.stylus_pressure_thickness = Some(self.input_state.current_thickness);
168+
self.record_stylus_peak(self.input_state.current_thickness);
169+
} else {
170+
self.stylus_pressure_thickness = None;
171+
self.stylus_peak_thickness = None;
172+
}
173+
}
174+
}
155175
}

src/backend/wayland/state/render/ui.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ impl WaylandState {
127127
self.input_state.clear_color_picker_popup_layout();
128128
}
129129

130+
if self.input_state.is_radial_menu_open() {
131+
self.input_state.update_radial_menu_layout(width, height);
132+
crate::ui::render_radial_menu(ctx, &self.input_state, width, height);
133+
} else {
134+
self.input_state.clear_radial_menu_layout();
135+
}
136+
130137
self.input_state.ui_toast_bounds =
131138
crate::ui::render_ui_toast(ctx, &self.input_state, width, height);
132139
crate::ui::render_preset_toast(ctx, &self.input_state, width, height);

src/config/action_meta/entries/ui.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ pub const ENTRIES: &[ActionMeta] = &[
6161
true,
6262
false
6363
),
64+
meta!(
65+
ToggleRadialMenu,
66+
"Radial Menu",
67+
None,
68+
"Toggle radial menu at cursor",
69+
UI,
70+
true,
71+
false,
72+
false,
73+
&["pie menu"]
74+
),
6475
meta!(
6576
ToggleSelectionProperties,
6677
"Selection Properties",

src/config/enums.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ pub enum StatusPosition {
2121
BottomRight,
2222
}
2323

24+
/// Mouse button used to toggle the radial menu.
25+
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema)]
26+
#[serde(rename_all = "kebab-case")]
27+
pub enum RadialMenuMouseBinding {
28+
/// Toggle radial menu with middle click.
29+
Middle,
30+
/// Toggle radial menu with right click.
31+
Right,
32+
/// Disable mouse-button toggling (keyboard action only).
33+
Disabled,
34+
}
35+
2436
/// Color specification - either a named color or RGB values.
2537
///
2638
/// # Examples

src/config/keybindings/actions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ pub enum Action {
109109
TogglePresenterMode,
110110
ToggleHighlightTool,
111111
ToggleFill,
112+
ToggleRadialMenu,
112113
ToggleSelectionProperties,
113114
OpenContextMenu,
114115

0 commit comments

Comments
 (0)