From 9884d4584f378dbd48cd87d0ea085b7164ec3e74 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:55:00 +0100 Subject: [PATCH] feat: make drag tool mappings configurable Add configurable [drawing] drag/modifier tool bindings with legacy defaults. Wire runtime + configurator support, docs/examples, and tests. Fix highlight active-state for drag-mapped highlight tool. --- README.md | 2 + config.example.toml | 8 ++ configurator/src/app/update/fields.rs | 17 ++++- configurator/src/app/update/mod.rs | 3 + configurator/src/app/view/drawing/mod.rs | 56 +++++++++++++- configurator/src/messages.rs | 12 +-- .../src/models/config/draft/from_config.rs | 9 ++- configurator/src/models/config/draft/mod.rs | 7 +- configurator/src/models/config/setters.rs | 14 +++- configurator/src/models/config/tests.rs | 41 ++++++++++- .../src/models/config/to_config/drawing.rs | 5 ++ configurator/src/models/fields/mod.rs | 2 +- configurator/src/models/fields/tool.rs | 21 ++++++ configurator/src/models/mod.rs | 2 +- docs/CONFIG.md | 8 ++ .../wayland/backend/state_init/input_state.rs | 31 +++++++- src/config/tests/validate.rs | 14 ++++ src/config/types/arrow.rs | 2 +- src/config/types/drawing.rs | 47 +++++++++++- src/input/mod.rs | 2 +- src/input/modifiers.rs | 73 ++++++++++++++++--- src/input/state/core/base/state/init.rs | 7 +- src/input/state/core/base/state/structs.rs | 4 +- src/input/state/core/highlight_controls.rs | 18 ++--- .../state/core/tool_controls/settings.rs | 16 +++- src/input/state/tests/drawing.rs | 46 ++++++++++++ src/input/tool.rs | 2 +- 27 files changed, 425 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index bafa8104..970ff614 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,8 @@ Press F1 for the complete in-app cheat sheet. | Text mode | T, Click to position, type, Enter to finish | | Sticky note | N, Click to place, type, Enter to finish | +Drag modifier mappings are configurable in `config.toml` via `[drawing]` (`drag_tool`, `shift_drag_tool`, `ctrl_drag_tool`, `ctrl_shift_drag_tool`, `tab_drag_tool`) or in the configurator Drawing tab. +
diff --git a/config.example.toml b/config.example.toml index cdb7c668..ab42ade0 100644 --- a/config.example.toml +++ b/config.example.toml @@ -666,6 +666,14 @@ hit_test_linear_threshold = 400 # Undo history size (10 - 1000) undo_stack_limit = 100 +# Drag gesture tool mapping (defaults match existing behavior) +# Allowed values use kebab-case tool names, e.g. "pen", "arrow", "eraser" +drag_tool = "pen" +shift_drag_tool = "line" +ctrl_drag_tool = "rect" +ctrl_shift_drag_tool = "arrow" +tab_drag_tool = "ellipse" + # ─────────────────────────────────────────────────────────────────────────────── # Font Configuration (Pango-based text rendering) # ─────────────────────────────────────────────────────────────────────────────── diff --git a/configurator/src/app/update/fields.rs b/configurator/src/app/update/fields.rs index fc9d8be9..1b21e305 100644 --- a/configurator/src/app/update/fields.rs +++ b/configurator/src/app/update/fields.rs @@ -2,10 +2,10 @@ use iced::Command; use crate::messages::Message; use crate::models::{ - ColorMode, ColorPickerId, EraserModeOption, FontStyleOption, FontWeightOption, KeybindingField, - NamedColorOption, OverrideOption, PresenterToolBehaviorOption, QuadField, + ColorMode, ColorPickerId, DragToolField, EraserModeOption, FontStyleOption, FontWeightOption, + KeybindingField, NamedColorOption, OverrideOption, PresenterToolBehaviorOption, QuadField, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, TextField, - ToggleField, ToolbarLayoutModeOption, ToolbarOverrideField, TripletField, + ToggleField, ToolOption, ToolbarLayoutModeOption, ToolbarOverrideField, TripletField, }; #[cfg(feature = "tablet-input")] use crate::models::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; @@ -105,6 +105,17 @@ impl ConfiguratorApp { Command::none() } + pub(super) fn handle_drawing_drag_tool_changed( + &mut self, + field: DragToolField, + option: ToolOption, + ) -> Command { + self.status = StatusMessage::idle(); + self.draft.set_drag_tool(field, option); + self.refresh_dirty_flag(); + Command::none() + } + pub(super) fn handle_status_position_changed( &mut self, option: StatusPositionOption, diff --git a/configurator/src/app/update/mod.rs b/configurator/src/app/update/mod.rs index 9d2dd12d..7dee1074 100644 --- a/configurator/src/app/update/mod.rs +++ b/configurator/src/app/update/mod.rs @@ -41,6 +41,9 @@ impl ConfiguratorApp { Message::ColorModeChanged(mode) => self.handle_color_mode_changed(mode), Message::NamedColorSelected(option) => self.handle_named_color_selected(option), Message::EraserModeChanged(option) => self.handle_eraser_mode_changed(option), + Message::DrawingDragToolChanged(field, option) => { + self.handle_drawing_drag_tool_changed(field, option) + } Message::StatusPositionChanged(option) => self.handle_status_position_changed(option), Message::ToolbarLayoutModeChanged(option) => { self.handle_toolbar_layout_mode_changed(option) diff --git a/configurator/src/app/view/drawing/mod.rs b/configurator/src/app/view/drawing/mod.rs index 432abd3e..efc6ada1 100644 --- a/configurator/src/app/view/drawing/mod.rs +++ b/configurator/src/app/view/drawing/mod.rs @@ -5,7 +5,7 @@ use iced::widget::{column, pick_list, row, scrollable, text}; use iced::{Element, Length}; use crate::messages::Message; -use crate::models::{EraserModeOption, TextField, ToggleField}; +use crate::models::{DragToolField, EraserModeOption, TextField, ToggleField, ToolOption}; use self::color::drawing_color_block; use self::font::font_controls; @@ -22,6 +22,12 @@ impl ConfiguratorApp { Some(self.draft.drawing_default_eraser_mode), Message::EraserModeChanged, ); + let drag_tool_pick = |field: DragToolField, selected: ToolOption| { + pick_list(ToolOption::list(), Some(selected), move |option| { + Message::DrawingDragToolChanged(field, option) + }) + .width(Length::Fill) + }; let column = column![ text("Drawing Defaults").size(20), @@ -66,6 +72,54 @@ impl ConfiguratorApp { ) ] .spacing(12), + text("Drag Tool Mapping").size(16), + row![ + labeled_control( + DragToolField::Drag.label(), + drag_tool_pick(DragToolField::Drag, self.draft.drawing_drag_tool).into(), + self.defaults.drawing_drag_tool.label().to_string(), + self.draft.drawing_drag_tool != self.defaults.drawing_drag_tool, + ), + labeled_control( + DragToolField::ShiftDrag.label(), + drag_tool_pick(DragToolField::ShiftDrag, self.draft.drawing_shift_drag_tool) + .into(), + self.defaults.drawing_shift_drag_tool.label().to_string(), + self.draft.drawing_shift_drag_tool != self.defaults.drawing_shift_drag_tool, + ) + ] + .spacing(12), + row![ + labeled_control( + DragToolField::CtrlDrag.label(), + drag_tool_pick(DragToolField::CtrlDrag, self.draft.drawing_ctrl_drag_tool) + .into(), + self.defaults.drawing_ctrl_drag_tool.label().to_string(), + self.draft.drawing_ctrl_drag_tool != self.defaults.drawing_ctrl_drag_tool, + ), + labeled_control( + DragToolField::CtrlShiftDrag.label(), + drag_tool_pick( + DragToolField::CtrlShiftDrag, + self.draft.drawing_ctrl_shift_drag_tool, + ) + .into(), + self.defaults + .drawing_ctrl_shift_drag_tool + .label() + .to_string(), + self.draft.drawing_ctrl_shift_drag_tool + != self.defaults.drawing_ctrl_shift_drag_tool, + ) + ] + .spacing(12), + row![labeled_control( + DragToolField::TabDrag.label(), + drag_tool_pick(DragToolField::TabDrag, self.draft.drawing_tab_drag_tool).into(), + self.defaults.drawing_tab_drag_tool.label().to_string(), + self.draft.drawing_tab_drag_tool != self.defaults.drawing_tab_drag_tool, + )] + .spacing(12), row![ labeled_input_with_feedback( "Marker opacity (0.05-0.9)", diff --git a/configurator/src/messages.rs b/configurator/src/messages.rs index 2668d5c6..a7c4ff8d 100644 --- a/configurator/src/messages.rs +++ b/configurator/src/messages.rs @@ -5,11 +5,12 @@ use wayscriber::config::Config; use crate::models::{ BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode, ColorPickerId, - ColorPickerValue, EraserModeOption, FontStyleOption, FontWeightOption, KeybindingField, - KeybindingsTabId, NamedColorOption, OverrideOption, PresenterToolBehaviorOption, - PresetEraserKindOption, PresetEraserModeOption, PresetTextField, PresetToggleField, QuadField, - SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, TabId, TextField, - ToggleField, ToolOption, ToolbarLayoutModeOption, ToolbarOverrideField, TripletField, UiTabId, + ColorPickerValue, DragToolField, EraserModeOption, FontStyleOption, FontWeightOption, + KeybindingField, KeybindingsTabId, NamedColorOption, OverrideOption, + PresenterToolBehaviorOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField, + PresetToggleField, QuadField, SessionCompressionOption, SessionStorageModeOption, + StatusPositionOption, TabId, TextField, ToggleField, ToolOption, ToolbarLayoutModeOption, + ToolbarOverrideField, TripletField, UiTabId, }; #[cfg(feature = "tablet-input")] use crate::models::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; @@ -35,6 +36,7 @@ pub enum Message { ColorModeChanged(ColorMode), NamedColorSelected(NamedColorOption), EraserModeChanged(EraserModeOption), + DrawingDragToolChanged(DragToolField, ToolOption), StatusPositionChanged(StatusPositionOption), ToolbarLayoutModeChanged(ToolbarLayoutModeOption), ToolbarOverrideModeChanged(ToolbarLayoutModeOption), diff --git a/configurator/src/models/config/draft/from_config.rs b/configurator/src/models/config/draft/from_config.rs index 6c57b290..9b7cc651 100644 --- a/configurator/src/models/config/draft/from_config.rs +++ b/configurator/src/models/config/draft/from_config.rs @@ -1,7 +1,7 @@ use super::super::super::color::{ColorInput, ColorQuadInput}; use super::super::super::fields::{ EraserModeOption, FontStyleOption, FontWeightOption, PresenterToolBehaviorOption, - SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, + SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, ToolOption, ToolbarLayoutModeOption, }; #[cfg(feature = "tablet-input")] @@ -38,6 +38,13 @@ impl ConfigDraft { drawing_font_style: style_value, drawing_text_background_enabled: config.drawing.text_background_enabled, drawing_default_fill_enabled: config.drawing.default_fill_enabled, + drawing_drag_tool: ToolOption::from_tool(config.drawing.drag_tool), + drawing_shift_drag_tool: ToolOption::from_tool(config.drawing.shift_drag_tool), + drawing_ctrl_drag_tool: ToolOption::from_tool(config.drawing.ctrl_drag_tool), + drawing_ctrl_shift_drag_tool: ToolOption::from_tool( + config.drawing.ctrl_shift_drag_tool, + ), + drawing_tab_drag_tool: ToolOption::from_tool(config.drawing.tab_drag_tool), drawing_font_style_option: style_option, drawing_font_weight_option: weight_option, diff --git a/configurator/src/models/config/draft/mod.rs b/configurator/src/models/config/draft/mod.rs index 80c69727..64fc0e48 100644 --- a/configurator/src/models/config/draft/mod.rs +++ b/configurator/src/models/config/draft/mod.rs @@ -3,7 +3,7 @@ mod from_config; use super::super::color::{ColorInput, ColorQuadInput}; use super::super::fields::{ EraserModeOption, FontStyleOption, FontWeightOption, PresenterToolBehaviorOption, - SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, + SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, ToolOption, ToolbarLayoutModeOption, }; #[cfg(feature = "tablet-input")] @@ -29,6 +29,11 @@ pub struct ConfigDraft { pub drawing_font_style: String, pub drawing_text_background_enabled: bool, pub drawing_default_fill_enabled: bool, + pub drawing_drag_tool: ToolOption, + pub drawing_shift_drag_tool: ToolOption, + pub drawing_ctrl_drag_tool: ToolOption, + pub drawing_ctrl_shift_drag_tool: ToolOption, + pub drawing_tab_drag_tool: ToolOption, pub drawing_font_style_option: FontStyleOption, pub drawing_font_weight_option: FontWeightOption, diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs index 8037438a..dabaff87 100644 --- a/configurator/src/models/config/setters.rs +++ b/configurator/src/models/config/setters.rs @@ -1,6 +1,6 @@ use super::super::fields::{ - FontStyleOption, FontWeightOption, OverrideOption, QuadField, TextField, ToggleField, - ToolbarLayoutModeOption, ToolbarOverrideField, TripletField, + DragToolField, FontStyleOption, FontWeightOption, OverrideOption, QuadField, TextField, + ToggleField, ToolOption, ToolbarLayoutModeOption, ToolbarOverrideField, TripletField, }; use super::draft::ConfigDraft; @@ -30,6 +30,16 @@ impl ConfigDraft { .set(field, value); } + pub fn set_drag_tool(&mut self, field: DragToolField, value: ToolOption) { + match field { + DragToolField::Drag => self.drawing_drag_tool = value, + DragToolField::ShiftDrag => self.drawing_shift_drag_tool = value, + DragToolField::CtrlDrag => self.drawing_ctrl_drag_tool = value, + DragToolField::CtrlShiftDrag => self.drawing_ctrl_shift_drag_tool = value, + DragToolField::TabDrag => self.drawing_tab_drag_tool = value, + } + } + pub fn set_toggle(&mut self, field: ToggleField, value: bool) { match field { ToggleField::DrawingTextBackground => { diff --git a/configurator/src/models/config/tests.rs b/configurator/src/models/config/tests.rs index 38b437e7..42666cf2 100644 --- a/configurator/src/models/config/tests.rs +++ b/configurator/src/models/config/tests.rs @@ -1,6 +1,7 @@ use super::super::color::ColorInput; use super::super::fields::{ - FontWeightOption, QuadField, SessionStorageModeOption, TextField, ToggleField, TripletField, + FontWeightOption, QuadField, SessionStorageModeOption, TextField, ToggleField, ToolOption, + TripletField, }; use super::super::{ColorMode, NamedColorOption}; use super::ConfigDraft; @@ -131,3 +132,41 @@ fn config_draft_round_trips_presets_and_history() { assert_eq!(round_trip.presets.slot_count, config.presets.slot_count); assert_eq!(round_trip.presets.get_slot(1), config.presets.get_slot(1)); } + +#[test] +fn config_draft_round_trips_drag_tool_mapping() { + let mut config = Config::default(); + config.drawing.drag_tool = Tool::Arrow; + config.drawing.shift_drag_tool = Tool::Eraser; + config.drawing.ctrl_drag_tool = Tool::Pen; + config.drawing.ctrl_shift_drag_tool = Tool::Rect; + config.drawing.tab_drag_tool = Tool::Ellipse; + + let draft = ConfigDraft::from_config(&config); + assert_eq!(draft.drawing_drag_tool, ToolOption::Arrow); + assert_eq!(draft.drawing_shift_drag_tool, ToolOption::Eraser); + assert_eq!(draft.drawing_ctrl_drag_tool, ToolOption::Pen); + assert_eq!(draft.drawing_ctrl_shift_drag_tool, ToolOption::Rect); + assert_eq!(draft.drawing_tab_drag_tool, ToolOption::Ellipse); + + let round_trip = draft + .to_config(&config) + .expect("expected config to round trip"); + assert_eq!(round_trip.drawing.drag_tool, config.drawing.drag_tool); + assert_eq!( + round_trip.drawing.shift_drag_tool, + config.drawing.shift_drag_tool + ); + assert_eq!( + round_trip.drawing.ctrl_drag_tool, + config.drawing.ctrl_drag_tool + ); + assert_eq!( + round_trip.drawing.ctrl_shift_drag_tool, + config.drawing.ctrl_shift_drag_tool + ); + assert_eq!( + round_trip.drawing.tab_drag_tool, + config.drawing.tab_drag_tool + ); +} diff --git a/configurator/src/models/config/to_config/drawing.rs b/configurator/src/models/config/to_config/drawing.rs index 05f501f8..c932346c 100644 --- a/configurator/src/models/config/to_config/drawing.rs +++ b/configurator/src/models/config/to_config/drawing.rs @@ -39,6 +39,11 @@ impl ConfigDraft { config.drawing.font_style = self.drawing_font_style.clone(); config.drawing.text_background_enabled = self.drawing_text_background_enabled; config.drawing.default_fill_enabled = self.drawing_default_fill_enabled; + config.drawing.drag_tool = self.drawing_drag_tool.to_tool(); + config.drawing.shift_drag_tool = self.drawing_shift_drag_tool.to_tool(); + config.drawing.ctrl_drag_tool = self.drawing_ctrl_drag_tool.to_tool(); + config.drawing.ctrl_shift_drag_tool = self.drawing_ctrl_shift_drag_tool.to_tool(); + config.drawing.tab_drag_tool = self.drawing_tab_drag_tool.to_tool(); parse_field( &self.drawing_hit_test_tolerance, "drawing.hit_test_tolerance", diff --git a/configurator/src/models/fields/mod.rs b/configurator/src/models/fields/mod.rs index 4f4f81a0..1f751ee7 100644 --- a/configurator/src/models/fields/mod.rs +++ b/configurator/src/models/fields/mod.rs @@ -19,7 +19,7 @@ pub use status::StatusPositionOption; pub use toggles::{ PresetTextField, PresetToggleField, QuadField, TextField, ToggleField, TripletField, }; -pub use tool::ToolOption; +pub use tool::{DragToolField, ToolOption}; pub use toolbar::{OverrideOption, ToolbarLayoutModeOption, ToolbarOverrideField}; #[cfg(test)] diff --git a/configurator/src/models/fields/tool.rs b/configurator/src/models/fields/tool.rs index 415ecdbc..fcab813d 100644 --- a/configurator/src/models/fields/tool.rs +++ b/configurator/src/models/fields/tool.rs @@ -1,5 +1,26 @@ use wayscriber::input::Tool; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DragToolField { + Drag, + ShiftDrag, + CtrlDrag, + CtrlShiftDrag, + TabDrag, +} + +impl DragToolField { + pub fn label(self) -> &'static str { + match self { + Self::Drag => "Drag", + Self::ShiftDrag => "Shift+Drag", + Self::CtrlDrag => "Ctrl+Drag", + Self::CtrlShiftDrag => "Ctrl+Shift+Drag", + Self::TabDrag => "Tab+Drag", + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ToolOption { Select, diff --git a/configurator/src/models/mod.rs b/configurator/src/models/mod.rs index d8567712..acc5caf3 100644 --- a/configurator/src/models/mod.rs +++ b/configurator/src/models/mod.rs @@ -11,7 +11,7 @@ pub use color::{ColorMode, ColorQuadInput, ColorTripletInput, NamedColorOption}; pub use color_picker::{ColorPickerId, ColorPickerValue}; pub use config::{BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ConfigDraft}; pub use fields::{ - EraserModeOption, FontStyleOption, FontWeightOption, OverrideOption, + DragToolField, EraserModeOption, FontStyleOption, FontWeightOption, OverrideOption, PresenterToolBehaviorOption, PresetEraserKindOption, PresetEraserModeOption, PresetTextField, PresetToggleField, QuadField, SessionCompressionOption, SessionStorageModeOption, StatusPositionOption, TextField, ToggleField, ToolOption, ToolbarLayoutModeOption, diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 565da1b7..0310dd32 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -61,6 +61,13 @@ text_background_enabled = false hit_test_tolerance = 6.0 hit_test_linear_threshold = 400 undo_stack_limit = 100 + +# Drag gesture tool mapping +drag_tool = "pen" +shift_drag_tool = "line" +ctrl_drag_tool = "rect" +ctrl_shift_drag_tool = "arrow" +tab_drag_tool = "ellipse" ``` **Color Options:** @@ -86,6 +93,7 @@ undo_stack_limit = 100 - Text background: false - Hit-test tolerance: 6.0px (linear threshold: 400) - Undo stack limit: 100 +- Drag mapping: Drag=Pen, Shift+Drag=Line, Ctrl+Drag=Rect, Ctrl+Shift+Drag=Arrow, Tab+Drag=Ellipse ### `[arrow]` - Arrow Geometry diff --git a/src/backend/wayland/backend/state_init/input_state.rs b/src/backend/wayland/backend/state_init/input_state.rs index b83324f8..9a03dc4f 100644 --- a/src/backend/wayland/backend/state_init/input_state.rs +++ b/src/backend/wayland/backend/state_init/input_state.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::config::{Action, Config, KeyBinding, KeybindingsConfig}; use crate::draw::FontDescriptor; -use crate::input::{ClickHighlightSettings, InputState}; +use crate::input::{ClickHighlightSettings, InputState, modifiers::DragToolBindings}; pub(super) fn build_input_state(config: &Config) -> InputState { let font_descriptor = FontDescriptor::new( @@ -43,6 +43,13 @@ pub(super) fn build_input_state(config: &Config) -> InputState { config.presenter_mode.clone(), ); input_state.set_action_bindings(action_bindings); + input_state.set_drag_tool_bindings(DragToolBindings { + drag: config.drawing.drag_tool, + shift_drag: config.drawing.shift_drag_tool, + ctrl_drag: config.drawing.ctrl_drag_tool, + ctrl_shift_drag: config.drawing.ctrl_shift_drag_tool, + tab_drag: config.drawing.tab_drag_tool, + }); input_state.set_hit_test_tolerance(config.drawing.hit_test_tolerance); input_state.set_hit_test_threshold(config.drawing.hit_test_linear_threshold); @@ -183,4 +190,26 @@ mod tests { assert!(input.show_active_output_badge); assert_eq!(input.command_palette_toast_duration_ms, 1234); } + + #[test] + fn build_input_state_applies_drag_tool_bindings() { + let mut config = Config::default(); + config.drawing.drag_tool = crate::input::Tool::Arrow; + config.drawing.shift_drag_tool = crate::input::Tool::Eraser; + config.drawing.ctrl_drag_tool = crate::input::Tool::Pen; + config.drawing.ctrl_shift_drag_tool = crate::input::Tool::Rect; + config.drawing.tab_drag_tool = crate::input::Tool::Ellipse; + + let input = build_input_state(&config); + assert_eq!( + input.drag_tool_bindings, + crate::input::DragToolBindings { + drag: crate::input::Tool::Arrow, + shift_drag: crate::input::Tool::Eraser, + ctrl_drag: crate::input::Tool::Pen, + ctrl_shift_drag: crate::input::Tool::Rect, + tab_drag: crate::input::Tool::Ellipse, + } + ); + } } diff --git a/src/config/tests/validate.rs b/src/config/tests/validate.rs index 1eb2b2f7..429f35d1 100644 --- a/src/config/tests/validate.rs +++ b/src/config/tests/validate.rs @@ -228,3 +228,17 @@ fn validate_does_not_clamp_autosave_interval_to_idle() { assert_eq!(config.session.autosave_idle_ms, 60_000); assert_eq!(config.session.autosave_interval_ms, 5_000); } + +#[test] +fn drawing_drag_tool_defaults_match_legacy_mapping() { + let config = Config::default(); + + assert_eq!(config.drawing.drag_tool, crate::input::Tool::Pen); + assert_eq!(config.drawing.shift_drag_tool, crate::input::Tool::Line); + assert_eq!(config.drawing.ctrl_drag_tool, crate::input::Tool::Rect); + assert_eq!( + config.drawing.ctrl_shift_drag_tool, + crate::input::Tool::Arrow + ); + assert_eq!(config.drawing.tab_drag_tool, crate::input::Tool::Ellipse); +} diff --git a/src/config/types/arrow.rs b/src/config/types/arrow.rs index 1e0a7cfb..6f9e39d9 100644 --- a/src/config/types/arrow.rs +++ b/src/config/types/arrow.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; /// Arrow drawing settings. /// -/// Controls the appearance of arrowheads when using the arrow tool (Ctrl+Shift+Drag). +/// Controls the appearance of arrowheads when using the arrow tool. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ArrowConfig { /// Arrowhead length in pixels (valid range: 5.0 - 50.0) diff --git a/src/config/types/drawing.rs b/src/config/types/drawing.rs index aa783079..52b245b2 100644 --- a/src/config/types/drawing.rs +++ b/src/config/types/drawing.rs @@ -1,5 +1,5 @@ use crate::config::enums::ColorSpec; -use crate::input::EraserMode; +use crate::input::{EraserMode, Tool}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -50,6 +50,26 @@ pub struct DrawingConfig { #[serde(default = "default_undo_stack_limit")] pub undo_stack_limit: usize, + /// Tool used for drag with no modifier. + #[serde(default = "default_drag_tool")] + pub drag_tool: Tool, + + /// Tool used for Shift+drag. + #[serde(default = "default_shift_drag_tool")] + pub shift_drag_tool: Tool, + + /// Tool used for Ctrl+drag. + #[serde(default = "default_ctrl_drag_tool")] + pub ctrl_drag_tool: Tool, + + /// Tool used for Ctrl+Shift+drag. + #[serde(default = "default_ctrl_shift_drag_tool")] + pub ctrl_shift_drag_tool: Tool, + + /// Tool used for Tab+drag. + #[serde(default = "default_tab_drag_tool")] + pub tab_drag_tool: Tool, + /// Font family name for text rendering (e.g., "Sans", "Monospace", "JetBrains Mono") /// Falls back to "Sans" if the specified font is not available /// Note: Install fonts system-wide and reference by family name @@ -83,6 +103,11 @@ impl Default for DrawingConfig { hit_test_tolerance: default_hit_test_tolerance(), hit_test_linear_threshold: default_hit_test_threshold(), undo_stack_limit: default_undo_stack_limit(), + drag_tool: default_drag_tool(), + shift_drag_tool: default_shift_drag_tool(), + ctrl_drag_tool: default_ctrl_drag_tool(), + ctrl_shift_drag_tool: default_ctrl_shift_drag_tool(), + tab_drag_tool: default_tab_drag_tool(), font_family: default_font_family(), font_weight: default_font_weight(), font_style: default_font_style(), @@ -146,3 +171,23 @@ fn default_hit_test_threshold() -> usize { fn default_undo_stack_limit() -> usize { 100 } + +fn default_drag_tool() -> Tool { + Tool::Pen +} + +fn default_shift_drag_tool() -> Tool { + Tool::Line +} + +fn default_ctrl_drag_tool() -> Tool { + Tool::Rect +} + +fn default_ctrl_shift_drag_tool() -> Tool { + Tool::Arrow +} + +fn default_tab_drag_tool() -> Tool { + Tool::Ellipse +} diff --git a/src/input/mod.rs b/src/input/mod.rs index 7f3a49a3..83008a36 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -32,4 +32,4 @@ pub use tool::{EraserMode, Tool}; // Re-export for public API (unused internally but part of public interface) #[allow(unused_imports)] -pub use modifiers::Modifiers; +pub use modifiers::{DragModifier, DragToolBindings, Modifiers}; diff --git a/src/input/modifiers.rs b/src/input/modifiers.rs index d840cb69..eecbdf27 100644 --- a/src/input/modifiers.rs +++ b/src/input/modifiers.rs @@ -2,6 +2,56 @@ use super::tool::Tool; +/// Active drag modifier combination used for tool mapping. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DragModifier { + None, + Shift, + Ctrl, + CtrlShift, + Tab, +} + +impl DragModifier { + pub fn is_active(self) -> bool { + !matches!(self, Self::None) + } +} + +/// Tool mapping for drag gestures with optional modifiers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DragToolBindings { + pub drag: Tool, + pub shift_drag: Tool, + pub ctrl_drag: Tool, + pub ctrl_shift_drag: Tool, + pub tab_drag: Tool, +} + +impl Default for DragToolBindings { + fn default() -> Self { + Self { + drag: Tool::Pen, + shift_drag: Tool::Line, + ctrl_drag: Tool::Rect, + ctrl_shift_drag: Tool::Arrow, + tab_drag: Tool::Ellipse, + } + } +} + +impl DragToolBindings { + pub fn tool_for_modifier(self, modifier: DragModifier) -> Tool { + match modifier { + DragModifier::None => self.drag, + DragModifier::Shift => self.shift_drag, + DragModifier::Ctrl => self.ctrl_drag, + DragModifier::CtrlShift => self.ctrl_shift_drag, + DragModifier::Tab => self.tab_drag, + } + } +} + /// Keyboard modifier state. /// /// Tracks which modifier keys (Shift, Ctrl, Alt, Tab) are currently pressed. @@ -35,25 +85,30 @@ impl Modifiers { } } - /// Determines which drawing tool is active based on current modifier state. + /// Returns the active drag modifier combination using the default priority. /// - /// # Tool Selection Priority + /// # Priority /// 1. Ctrl+Shift → Arrow /// 2. Ctrl → Rectangle /// 3. Shift → Line /// 4. Tab → Ellipse - /// 5. None → Pen (default) - pub fn current_tool(&self) -> Tool { + /// 5. None + pub fn active_drag_modifier(&self) -> DragModifier { if self.ctrl && self.shift { - Tool::Arrow + DragModifier::CtrlShift } else if self.ctrl { - Tool::Rect + DragModifier::Ctrl } else if self.shift { - Tool::Line + DragModifier::Shift } else if self.tab { - Tool::Ellipse + DragModifier::Tab } else { - Tool::Pen + DragModifier::None } } + + /// Determines which drawing tool is active based on current modifier state and drag mapping. + pub fn current_tool_with_bindings(&self, bindings: DragToolBindings) -> Tool { + bindings.tool_for_modifier(self.active_drag_modifier()) + } } diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index e4e6903a..9465047f 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -11,7 +11,11 @@ use super::structs::InputState; use crate::config::{Action, BoardsConfig, KeyBinding, PRESET_SLOTS_MAX, RadialMenuMouseBinding}; use crate::draw::{DirtyTracker, EraserKind, FontDescriptor}; use crate::input::state::highlight::{ClickHighlightSettings, ClickHighlightState}; -use crate::input::{BoardManager, modifiers::Modifiers, tool::EraserMode}; +use crate::input::{ + BoardManager, + modifiers::{DragToolBindings, Modifiers}, + tool::EraserMode, +}; use std::collections::HashMap; impl InputState { @@ -88,6 +92,7 @@ impl InputState { arrow_label_counter: 1, step_marker_counter: 1, modifiers: Modifiers::new(), + drag_tool_bindings: DragToolBindings::default(), state: DrawingState::Idle, should_exit: false, needs_redraw: true, diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index a4f27197..b9ec7083 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -27,7 +27,7 @@ use crate::input::BoardManager; use crate::input::boards::BoardState; use crate::input::state::highlight::ClickHighlightState; use crate::input::{ - modifiers::Modifiers, + modifiers::{DragToolBindings, Modifiers}, tool::{EraserMode, Tool}, }; use crate::util::Rect; @@ -93,6 +93,8 @@ pub struct InputState { pub step_marker_counter: u32, /// Current modifier key state pub modifiers: Modifiers, + /// Tool mapping for drag gestures with modifier keys + pub drag_tool_bindings: DragToolBindings, /// Current drawing mode state machine pub state: DrawingState, /// Whether user requested to exit the overlay diff --git a/src/input/state/core/highlight_controls.rs b/src/input/state/core/highlight_controls.rs index 3a266974..3db55e56 100644 --- a/src/input/state/core/highlight_controls.rs +++ b/src/input/state/core/highlight_controls.rs @@ -84,15 +84,18 @@ impl InputState { return *tool; } - let modifier_tool = self.modifiers.current_tool(); + let drag_modifier = self.modifiers.active_drag_modifier(); + let modifier_tool = self + .modifiers + .current_tool_with_bindings(self.drag_tool_bindings); if let Some(override_tool) = self.tool_override { if matches!(override_tool, Tool::Highlight | Tool::Eraser) { return override_tool; } - // Allow temporary modifier-based tools when the override is a drawing tool - if modifier_tool != Tool::Pen && modifier_tool != override_tool { + // Allow temporary modifier-based tools when an override is active. + if drag_modifier.is_active() && modifier_tool != override_tool { return modifier_tool; } @@ -104,14 +107,7 @@ impl InputState { /// Returns whether the highlight tool is currently selected. pub fn highlight_tool_active(&self) -> bool { - matches!(self.tool_override, Some(Tool::Highlight)) - || matches!( - self.state, - DrawingState::Drawing { - tool: Tool::Highlight, - .. - } - ) + self.active_tool() == Tool::Highlight } /// Sets highlight-only tool mode on/off and keeps click highlight in sync. diff --git a/src/input/state/core/tool_controls/settings.rs b/src/input/state/core/tool_controls/settings.rs index bbad9f5f..f071bbd8 100644 --- a/src/input/state/core/tool_controls/settings.rs +++ b/src/input/state/core/tool_controls/settings.rs @@ -1,6 +1,9 @@ use super::super::base::{DrawingState, InputState, MAX_STROKE_THICKNESS, MIN_STROKE_THICKNESS}; use crate::draw::{Color, FontDescriptor}; -use crate::input::tool::{EraserMode, Tool}; +use crate::input::{ + modifiers::DragToolBindings, + tool::{EraserMode, Tool}, +}; impl InputState { /// Sets or clears an explicit tool override. Returns true if the tool changed. @@ -53,6 +56,17 @@ impl InputState { self.tool_override } + /// Sets drag modifier -> tool mappings. Returns true if changed. + pub fn set_drag_tool_bindings(&mut self, bindings: DragToolBindings) -> bool { + if self.drag_tool_bindings == bindings { + return false; + } + self.drag_tool_bindings = bindings; + self.dirty_tracker.mark_full(); + self.needs_redraw = true; + true + } + /// Sets thickness or eraser size depending on the active tool. pub fn set_thickness_for_active_tool(&mut self, value: f64) -> bool { match self.active_tool() { diff --git a/src/input/state/tests/drawing.rs b/src/input/state/tests/drawing.rs index 6c3f64c8..662ab092 100644 --- a/src/input/state/tests/drawing.rs +++ b/src/input/state/tests/drawing.rs @@ -1,4 +1,5 @@ use super::*; +use crate::input::DragToolBindings; #[test] fn mouse_drag_creates_shapes_for_each_tool() { @@ -43,6 +44,51 @@ fn mouse_drag_creates_shapes_for_each_tool() { assert_eq!(state.boards.active_frame().shapes.len(), 5); } +#[test] +fn custom_drag_bindings_remap_default_and_modifier_tools() { + let mut state = create_test_input_state(); + assert!(state.set_drag_tool_bindings(DragToolBindings { + drag: Tool::Arrow, + shift_drag: Tool::Eraser, + ctrl_drag: Tool::Pen, + ctrl_shift_drag: Tool::Rect, + tab_drag: Tool::Ellipse, + })); + + assert_eq!(state.active_tool(), Tool::Arrow); + assert!(state.set_tool_override(Some(Tool::Arrow))); + assert_eq!(state.active_tool(), Tool::Arrow); + + state.modifiers.ctrl = true; + assert_eq!(state.active_tool(), Tool::Pen); + + state.modifiers.ctrl = false; + state.modifiers.shift = true; + assert_eq!(state.active_tool(), Tool::Eraser); + + state.modifiers.ctrl = true; + assert_eq!(state.active_tool(), Tool::Rect); +} + +#[test] +fn drag_mapped_highlight_reports_highlight_active() { + let mut state = create_test_input_state(); + assert!(state.set_drag_tool_bindings(DragToolBindings { + drag: Tool::Highlight, + shift_drag: Tool::Line, + ctrl_drag: Tool::Rect, + ctrl_shift_drag: Tool::Arrow, + tab_drag: Tool::Ellipse, + })); + + assert_eq!(state.active_tool(), Tool::Highlight); + assert!(state.highlight_tool_active()); + + state.modifiers.shift = true; + assert_eq!(state.active_tool(), Tool::Line); + assert!(!state.highlight_tool_active()); +} + #[test] fn toggle_click_highlight_action_changes_state() { let mut state = create_test_input_state(); diff --git a/src/input/tool.rs b/src/input/tool.rs index 5202e7ac..fb7ac859 100644 --- a/src/input/tool.rs +++ b/src/input/tool.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; /// Drawing tool selection. /// /// The active tool determines what shape is created when the user drags the mouse. -/// Tools are selected by holding modifier keys (Shift, Ctrl, Tab) while dragging. +/// Drag modifier mappings are configurable via `[drawing]` drag-tool fields. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum Tool {