From 45454d0a08b8306189de47a7efc79c687bb9cb09 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:23:43 +0100 Subject: [PATCH 1/4] feat: add step marker tool --- config.example.toml | 5 ++ configurator/src/models/fields/tool.rs | 5 ++ .../src/models/keybindings/field/config.rs | 4 + .../src/models/keybindings/field/labels.rs | 4 + .../src/models/keybindings/field/list.rs | 2 + .../src/models/keybindings/field/mod.rs | 2 + .../src/models/keybindings/field/tab.rs | 4 +- docs/CONFIG.md | 5 ++ .../wayland/state/render/tool_preview.rs | 1 + .../wayland/toolbar/layout/spec/side/sizes.rs | 4 + .../toolbar/render/side_palette/mod.rs | 2 + .../side_palette/presets/slot/content.rs | 1 + .../render/side_palette/step_marker.rs | 70 +++++++++++++++++ .../render/top_strip/icons/tool_row.rs | 2 + .../wayland/toolbar/render/top_strip/text.rs | 9 ++- src/config/action_meta/entries/drawing.rs | 10 +++ src/config/action_meta/entries/tools.rs | 10 +++ src/config/action_meta/tests.rs | 3 + src/config/keybindings/actions.rs | 2 + src/config/keybindings/config/map/tools.rs | 8 ++ .../config/types/bindings/tools.rs | 8 ++ src/config/keybindings/defaults/tools.rs | 8 ++ src/draw/mod.rs | 4 +- src/draw/render/selection.rs | 21 +++++ src/draw/render/shapes.rs | 78 ++++++++++++++++++- src/draw/shape/mod.rs | 4 +- src/draw/shape/step_marker.rs | 43 ++++++++++ src/draw/shape/types.rs | 27 +++++++ src/input/hit_test/mod.rs | 9 ++- src/input/hit_test/shapes.rs | 13 ++++ src/input/state/actions/action_tools.rs | 8 ++ src/input/state/core/base/state/init.rs | 1 + src/input/state/core/base/state/structs.rs | 2 + src/input/state/core/dirty.rs | 12 ++- .../apply_selection/actions/color.rs | 4 +- src/input/state/core/properties/summary.rs | 3 +- .../translation/transform.rs | 4 + src/input/state/core/utility/mod.rs | 1 + src/input/state/core/utility/step_markers.rs | 49 ++++++++++++ src/input/state/mouse/release/drawing.rs | 15 ++++ src/input/state/render.rs | 6 ++ src/input/state/tests/mod.rs | 1 + src/input/state/tests/step_markers.rs | 48 ++++++++++++ src/input/tool.rs | 2 + src/session/snapshot/apply.rs | 1 + src/toolbar_icons/tools.rs | 33 ++++++++ src/ui/help_overlay/sections/builder.rs | 4 + src/ui/toolbar/apply/mod.rs | 3 + src/ui/toolbar/apply/tools.rs | 4 + src/ui/toolbar/bindings.rs | 2 + src/ui/toolbar/events.rs | 1 + src/ui/toolbar/snapshot.rs | 2 + 52 files changed, 565 insertions(+), 9 deletions(-) create mode 100644 src/backend/wayland/toolbar/render/side_palette/step_marker.rs create mode 100644 src/draw/shape/step_marker.rs create mode 100644 src/input/state/core/utility/step_markers.rs create mode 100644 src/input/state/tests/step_markers.rs diff --git a/config.example.toml b/config.example.toml index bbceb1a5..805a63e5 100644 --- a/config.example.toml +++ b/config.example.toml @@ -79,6 +79,7 @@ decrease_marker_opacity = ["Ctrl+Alt+ArrowDown"] select_selection_tool = ["V"] select_pen_tool = ["F"] select_marker_tool = ["H"] +select_step_marker_tool = [] select_eraser_tool = ["D"] # Toggle eraser behavior mode toggle_eraser_mode = ["Ctrl+Shift+E"] @@ -89,6 +90,10 @@ select_arrow_tool = [] select_highlight_tool = [] toggle_highlight_tool = ["Ctrl+Alt+H"] +# Reset label counters +reset_arrow_labels = ["Ctrl+Shift+R"] +reset_step_markers = [] + # Adjust font size increase_font_size = ["Ctrl+Shift++", "Ctrl+Shift+="] decrease_font_size = ["Ctrl+Shift+-", "Ctrl+Shift+_"] diff --git a/configurator/src/models/fields/tool.rs b/configurator/src/models/fields/tool.rs index 81bb4ed8..415ecdbc 100644 --- a/configurator/src/models/fields/tool.rs +++ b/configurator/src/models/fields/tool.rs @@ -9,6 +9,7 @@ pub enum ToolOption { Ellipse, Arrow, Marker, + StepMarker, Highlight, Eraser, } @@ -23,6 +24,7 @@ impl ToolOption { Self::Ellipse, Self::Arrow, Self::Marker, + Self::StepMarker, Self::Highlight, Self::Eraser, ] @@ -37,6 +39,7 @@ impl ToolOption { Self::Ellipse => "Ellipse", Self::Arrow => "Arrow", Self::Marker => "Marker", + Self::StepMarker => "Step", Self::Highlight => "Highlight", Self::Eraser => "Eraser", } @@ -51,6 +54,7 @@ impl ToolOption { Self::Ellipse => Tool::Ellipse, Self::Arrow => Tool::Arrow, Self::Marker => Tool::Marker, + Self::StepMarker => Tool::StepMarker, Self::Highlight => Tool::Highlight, Self::Eraser => Tool::Eraser, } @@ -65,6 +69,7 @@ impl ToolOption { Tool::Ellipse => Self::Ellipse, Tool::Arrow => Self::Arrow, Tool::Marker => Self::Marker, + Tool::StepMarker => Self::StepMarker, Tool::Highlight => Self::Highlight, Tool::Eraser => Self::Eraser, } diff --git a/configurator/src/models/keybindings/field/config.rs b/configurator/src/models/keybindings/field/config.rs index bd53f01c..6f9826b9 100644 --- a/configurator/src/models/keybindings/field/config.rs +++ b/configurator/src/models/keybindings/field/config.rs @@ -44,6 +44,7 @@ impl KeybindingField { Self::SelectEraserTool => &config.tools.select_eraser_tool, Self::ToggleEraserMode => &config.tools.toggle_eraser_mode, Self::SelectMarkerTool => &config.tools.select_marker_tool, + Self::SelectStepMarkerTool => &config.tools.select_step_marker_tool, Self::SelectLineTool => &config.tools.select_line_tool, Self::SelectRectTool => &config.tools.select_rect_tool, Self::SelectEllipseTool => &config.tools.select_ellipse_tool, @@ -111,6 +112,7 @@ impl KeybindingField { Self::ToggleZoomLock => &config.zoom.toggle_zoom_lock, Self::RefreshZoomCapture => &config.zoom.refresh_zoom_capture, Self::ResetArrowLabels => &config.tools.reset_arrow_labels, + Self::ResetStepMarkers => &config.tools.reset_step_markers, Self::ApplyPreset1 => &config.presets.apply_preset_1, Self::ApplyPreset2 => &config.presets.apply_preset_2, Self::ApplyPreset3 => &config.presets.apply_preset_3, @@ -171,6 +173,7 @@ impl KeybindingField { Self::SelectEraserTool => config.tools.select_eraser_tool = value, Self::ToggleEraserMode => config.tools.toggle_eraser_mode = value, Self::SelectMarkerTool => config.tools.select_marker_tool = value, + Self::SelectStepMarkerTool => config.tools.select_step_marker_tool = value, Self::SelectLineTool => config.tools.select_line_tool = value, Self::SelectRectTool => config.tools.select_rect_tool = value, Self::SelectEllipseTool => config.tools.select_ellipse_tool = value, @@ -238,6 +241,7 @@ impl KeybindingField { Self::ToggleZoomLock => config.zoom.toggle_zoom_lock = value, Self::RefreshZoomCapture => config.zoom.refresh_zoom_capture = value, Self::ResetArrowLabels => config.tools.reset_arrow_labels = value, + Self::ResetStepMarkers => config.tools.reset_step_markers = value, Self::ApplyPreset1 => config.presets.apply_preset_1 = value, Self::ApplyPreset2 => config.presets.apply_preset_2 = value, Self::ApplyPreset3 => config.presets.apply_preset_3 = value, diff --git a/configurator/src/models/keybindings/field/labels.rs b/configurator/src/models/keybindings/field/labels.rs index ecb8045f..df7ba046 100644 --- a/configurator/src/models/keybindings/field/labels.rs +++ b/configurator/src/models/keybindings/field/labels.rs @@ -39,6 +39,7 @@ impl KeybindingField { Self::SelectEraserTool => "Select eraser tool", Self::ToggleEraserMode => "Toggle eraser mode", Self::SelectMarkerTool => "Select marker tool", + Self::SelectStepMarkerTool => "Select step marker tool", Self::SelectLineTool => "Select line tool", Self::SelectRectTool => "Select rectangle tool", Self::SelectEllipseTool => "Select ellipse tool", @@ -106,6 +107,7 @@ impl KeybindingField { Self::ToggleZoomLock => "Toggle zoom lock", Self::RefreshZoomCapture => "Refresh zoom snapshot", Self::ResetArrowLabels => "Reset arrow labels", + Self::ResetStepMarkers => "Reset step markers", Self::ApplyPreset1 => "Apply preset 1", Self::ApplyPreset2 => "Apply preset 2", Self::ApplyPreset3 => "Apply preset 3", @@ -162,6 +164,7 @@ impl KeybindingField { Self::SelectEraserTool => "select_eraser_tool", Self::ToggleEraserMode => "toggle_eraser_mode", Self::SelectMarkerTool => "select_marker_tool", + Self::SelectStepMarkerTool => "select_step_marker_tool", Self::SelectLineTool => "select_line_tool", Self::SelectRectTool => "select_rect_tool", Self::SelectEllipseTool => "select_ellipse_tool", @@ -229,6 +232,7 @@ impl KeybindingField { Self::ToggleZoomLock => "toggle_zoom_lock", Self::RefreshZoomCapture => "refresh_zoom_capture", Self::ResetArrowLabels => "reset_arrow_labels", + Self::ResetStepMarkers => "reset_step_markers", Self::ApplyPreset1 => "apply_preset_1", Self::ApplyPreset2 => "apply_preset_2", Self::ApplyPreset3 => "apply_preset_3", diff --git a/configurator/src/models/keybindings/field/list.rs b/configurator/src/models/keybindings/field/list.rs index 132633d4..6560e8d0 100644 --- a/configurator/src/models/keybindings/field/list.rs +++ b/configurator/src/models/keybindings/field/list.rs @@ -39,6 +39,7 @@ impl KeybindingField { Self::SelectEraserTool, Self::ToggleEraserMode, Self::SelectMarkerTool, + Self::SelectStepMarkerTool, Self::SelectLineTool, Self::SelectRectTool, Self::SelectEllipseTool, @@ -106,6 +107,7 @@ impl KeybindingField { Self::ToggleZoomLock, Self::RefreshZoomCapture, Self::ResetArrowLabels, + Self::ResetStepMarkers, Self::ApplyPreset1, Self::ApplyPreset2, Self::ApplyPreset3, diff --git a/configurator/src/models/keybindings/field/mod.rs b/configurator/src/models/keybindings/field/mod.rs index 0c474d6d..45875d51 100644 --- a/configurator/src/models/keybindings/field/mod.rs +++ b/configurator/src/models/keybindings/field/mod.rs @@ -41,6 +41,7 @@ pub enum KeybindingField { SelectEraserTool, ToggleEraserMode, SelectMarkerTool, + SelectStepMarkerTool, SelectLineTool, SelectRectTool, SelectEllipseTool, @@ -93,6 +94,7 @@ pub enum KeybindingField { ToggleZoomLock, RefreshZoomCapture, ResetArrowLabels, + ResetStepMarkers, Board1, Board2, Board3, diff --git a/configurator/src/models/keybindings/field/tab.rs b/configurator/src/models/keybindings/field/tab.rs index fcdd7c0f..55cc3623 100644 --- a/configurator/src/models/keybindings/field/tab.rs +++ b/configurator/src/models/keybindings/field/tab.rs @@ -28,13 +28,15 @@ impl KeybindingField { | Self::SelectEraserTool | Self::ToggleEraserMode | Self::SelectMarkerTool + | Self::SelectStepMarkerTool | Self::SelectLineTool | Self::SelectRectTool | Self::SelectEllipseTool | Self::SelectArrowTool | Self::SelectHighlightTool | Self::ToggleHighlightTool - | Self::ResetArrowLabels => KeybindingsTabId::Tools, + | Self::ResetArrowLabels + | Self::ResetStepMarkers => KeybindingsTabId::Tools, Self::DuplicateSelection | Self::CopySelection | Self::PasteSelection diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 132517fa..6f1bd399 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -706,6 +706,7 @@ decrease_marker_opacity = ["Ctrl+Alt+ArrowDown"] select_selection_tool = ["V"] select_pen_tool = ["F"] select_marker_tool = ["H"] +select_step_marker_tool = [] select_eraser_tool = ["D"] toggle_eraser_mode = ["Ctrl+Shift+E"] select_line_tool = [] @@ -715,6 +716,10 @@ select_arrow_tool = [] select_highlight_tool = [] toggle_highlight_tool = ["Ctrl+Alt+H"] +# Reset label counters +reset_arrow_labels = ["Ctrl+Shift+R"] +reset_step_markers = [] + # Adjust font size increase_font_size = ["Ctrl+Shift++", "Ctrl+Shift+="] decrease_font_size = ["Ctrl+Shift+-", "Ctrl+Shift+_"] diff --git a/src/backend/wayland/state/render/tool_preview.rs b/src/backend/wayland/state/render/tool_preview.rs index a46dc4b8..00f138ed 100644 --- a/src/backend/wayland/state/render/tool_preview.rs +++ b/src/backend/wayland/state/render/tool_preview.rs @@ -55,6 +55,7 @@ pub(super) fn draw_tool_preview( Tool::Ellipse => toolbar_icons::draw_icon_circle(ctx, icon_x, icon_y, icon_size), Tool::Arrow => toolbar_icons::draw_icon_arrow(ctx, icon_x, icon_y, icon_size), Tool::Marker => toolbar_icons::draw_icon_marker(ctx, icon_x, icon_y, icon_size), + Tool::StepMarker => toolbar_icons::draw_icon_step_marker(ctx, icon_x, icon_y, icon_size), Tool::Highlight => toolbar_icons::draw_icon_highlight(ctx, icon_x, icon_y, icon_size), Tool::Eraser => toolbar_icons::draw_icon_eraser(ctx, icon_x, icon_y, icon_size), } diff --git a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs index 187f3718..3650c5c3 100644 --- a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs +++ b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs @@ -17,6 +17,7 @@ impl ToolbarLayoutSpec { snapshot.text_active || snapshot.note_active || snapshot.show_text_controls; let show_arrow_controls = snapshot.active_tool == Tool::Arrow || snapshot.arrow_label_enabled; + let show_step_marker_controls = snapshot.active_tool == Tool::StepMarker; let show_drawer_view = snapshot.drawer_open && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::View; let show_advanced = snapshot.show_actions_advanced && show_drawer_view; @@ -59,6 +60,9 @@ impl ToolbarLayoutSpec { }; add_section(arrow_height, &mut height); } + if show_step_marker_controls { + add_section(Self::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET, &mut height); + } if show_marker_opacity { add_section(Self::SIDE_SLIDER_CARD_HEIGHT, &mut height); } diff --git a/src/backend/wayland/toolbar/render/side_palette/mod.rs b/src/backend/wayland/toolbar/render/side_palette/mod.rs index 55284d45..53e30f04 100644 --- a/src/backend/wayland/toolbar/render/side_palette/mod.rs +++ b/src/backend/wayland/toolbar/render/side_palette/mod.rs @@ -8,6 +8,7 @@ mod marker; mod pages; mod presets; mod settings; +mod step_marker; mod step; mod text; mod thickness; @@ -93,6 +94,7 @@ pub fn render_side_palette( thickness::draw_thickness_section(&mut layout, &mut y); arrow::draw_arrow_section(&mut layout, &mut y); + step_marker::draw_step_marker_section(&mut layout, &mut y); marker::draw_marker_opacity_section(&mut layout, &mut y); text::draw_text_controls_section(&mut layout, &mut y); drawer::draw_drawer_tabs(&mut layout, &mut y); diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs b/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs index 2dece11b..1820967f 100644 --- a/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs +++ b/src/backend/wayland/toolbar/render/side_palette/presets/slot/content.rs @@ -122,6 +122,7 @@ fn draw_preset_icon(ctx: &cairo::Context, tool: Tool, x: f64, y: f64, size: f64) Tool::Ellipse => toolbar_icons::draw_icon_circle(ctx, x, y, size), Tool::Arrow => toolbar_icons::draw_icon_arrow(ctx, x, y, size), Tool::Marker => toolbar_icons::draw_icon_marker(ctx, x, y, size), + Tool::StepMarker => toolbar_icons::draw_icon_step_marker(ctx, x, y, size), Tool::Highlight => toolbar_icons::draw_icon_highlight(ctx, x, y, size), Tool::Eraser => toolbar_icons::draw_icon_eraser(ctx, x, y, size), } diff --git a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs new file mode 100644 index 00000000..e217a71d --- /dev/null +++ b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs @@ -0,0 +1,70 @@ +use super::super::widgets::constants::{COLOR_LABEL_HINT, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SMALL, set_color}; +use super::super::widgets::*; +use super::SidePaletteLayout; +use crate::backend::wayland::toolbar::events::HitKind; +use crate::backend::wayland::toolbar::hit::HitRegion; +use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; +use crate::input::Tool; +use crate::ui::toolbar::ToolbarEvent; +use crate::ui_text::{UiTextStyle, text_layout}; + +pub(super) fn draw_step_marker_section(layout: &mut SidePaletteLayout, y: &mut f64) { + let ctx = layout.ctx; + let snapshot = layout.snapshot; + let hits = &mut layout.hits; + let hover = layout.hover; + let x = layout.x; + let card_x = layout.card_x; + let card_w = layout.card_w; + let content_width = layout.content_width; + let section_gap = layout.section_gap; + let label_style = UiTextStyle { + family: FONT_FAMILY_DEFAULT, + slant: cairo::FontSlant::Normal, + weight: cairo::FontWeight::Bold, + size: FONT_SIZE_LABEL, + }; + let hint_style = UiTextStyle { + family: FONT_FAMILY_DEFAULT, + slant: cairo::FontSlant::Normal, + weight: cairo::FontWeight::Normal, + size: FONT_SIZE_SMALL, + }; + + if snapshot.active_tool != Tool::StepMarker { + return; + } + + let card_h = ToolbarLayoutSpec::SIDE_TOGGLE_CARD_HEIGHT_WITH_RESET; + draw_group_card(ctx, card_x, *y, card_w, card_h); + draw_section_label( + ctx, + label_style, + x, + *y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, + "Step markers", + ); + let hint = format!("Next: {}", snapshot.step_marker_next); + let layout_text = text_layout(ctx, hint_style, &hint, None); + let ext = layout_text.ink_extents(); + let hint_x = card_x + card_w - ext.width() - 8.0 - ext.x_bearing(); + let hint_y = *y + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL; + set_color(ctx, COLOR_LABEL_HINT); + layout_text.show_at_baseline(ctx, hint_x, hint_y); + + let reset_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; + let reset_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; + let reset_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, reset_y, content_width, reset_h)) + .unwrap_or(false); + draw_button(ctx, x, reset_y, content_width, reset_h, false, reset_hover); + draw_label_center(ctx, label_style, x, reset_y, content_width, reset_h, "Reset"); + hits.push(HitRegion { + rect: (x, reset_y, content_width, reset_h), + event: ToolbarEvent::ResetStepMarkerCounter, + kind: HitKind::Click, + tooltip: Some("Reset numbering to 1.".to_string()), + }); + + *y += card_h + section_gap; +} diff --git a/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs b/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs index b6b58518..a24017a4 100644 --- a/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs +++ b/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs @@ -34,6 +34,7 @@ pub(super) fn draw_tool_row( (Tool::Select, toolbar_icons::draw_icon_select as IconFn), (Tool::Pen, toolbar_icons::draw_icon_pen as IconFn), (Tool::Marker, toolbar_icons::draw_icon_marker as IconFn), + (Tool::StepMarker, toolbar_icons::draw_icon_step_marker as IconFn), (Tool::Eraser, toolbar_icons::draw_icon_eraser as IconFn), ] } else { @@ -41,6 +42,7 @@ pub(super) fn draw_tool_row( (Tool::Select, toolbar_icons::draw_icon_select as IconFn), (Tool::Pen, toolbar_icons::draw_icon_pen as IconFn), (Tool::Marker, toolbar_icons::draw_icon_marker as IconFn), + (Tool::StepMarker, toolbar_icons::draw_icon_step_marker as IconFn), (Tool::Eraser, toolbar_icons::draw_icon_eraser as IconFn), (Tool::Line, toolbar_icons::draw_icon_line as IconFn), (Tool::Rect, toolbar_icons::draw_icon_rect as IconFn), diff --git a/src/backend/wayland/toolbar/render/top_strip/text.rs b/src/backend/wayland/toolbar/render/top_strip/text.rs index aa241340..6bdc2ca9 100644 --- a/src/backend/wayland/toolbar/render/top_strip/text.rs +++ b/src/backend/wayland/toolbar/render/top_strip/text.rs @@ -41,12 +41,19 @@ pub(super) fn draw_text_strip( }; let tool_buttons: &[Tool] = if is_simple { - &[Tool::Select, Tool::Pen, Tool::Marker, Tool::Eraser] + &[ + Tool::Select, + Tool::Pen, + Tool::Marker, + Tool::StepMarker, + Tool::Eraser, + ] } else { &[ Tool::Select, Tool::Pen, Tool::Marker, + Tool::StepMarker, Tool::Eraser, Tool::Line, Tool::Rect, diff --git a/src/config/action_meta/entries/drawing.rs b/src/config/action_meta/entries/drawing.rs index 6408bd7c..f4887379 100644 --- a/src/config/action_meta/entries/drawing.rs +++ b/src/config/action_meta/entries/drawing.rs @@ -71,6 +71,16 @@ pub const ENTRIES: &[ActionMeta] = &[ false, false ), + meta!( + ResetStepMarkerCounter, + "Reset Step Markers", + None, + "Reset step marker counter", + Drawing, + false, + false, + false + ), meta!( ToggleFill, "Toggle Fill", diff --git a/src/config/action_meta/entries/tools.rs b/src/config/action_meta/entries/tools.rs index 97479840..5e2d01f9 100644 --- a/src/config/action_meta/entries/tools.rs +++ b/src/config/action_meta/entries/tools.rs @@ -111,6 +111,16 @@ pub const ENTRIES: &[ActionMeta] = &[ true, true ), + meta!( + SelectStepMarkerTool, + "Step Marker Tool", + Some("Steps"), + "Place numbered step markers", + Tools, + true, + true, + true + ), meta!( SelectEraserTool, "Eraser Tool", diff --git a/src/config/action_meta/tests.rs b/src/config/action_meta/tests.rs index 888840f7..d7f4cdf7 100644 --- a/src/config/action_meta/tests.rs +++ b/src/config/action_meta/tests.rs @@ -44,6 +44,7 @@ const HELP_ACTIONS: &[Action] = &[ Action::SelectArrowTool, Action::ToggleHighlightTool, Action::SelectMarkerTool, + Action::SelectStepMarkerTool, Action::SelectEraserTool, Action::IncreaseThickness, Action::DecreaseThickness, @@ -91,6 +92,7 @@ const TOOLBAR_ACTIONS: &[Action] = &[ Action::SelectArrowTool, Action::SelectSelectionTool, Action::SelectMarkerTool, + Action::SelectStepMarkerTool, Action::SelectHighlightTool, Action::SelectEraserTool, Action::EnterTextMode, @@ -147,6 +149,7 @@ const PALETTE_ACTIONS: &[Action] = &[ Action::SelectArrowTool, Action::SelectHighlightTool, Action::SelectMarkerTool, + Action::SelectStepMarkerTool, Action::SelectEraserTool, Action::ToggleEraserMode, Action::IncreaseThickness, diff --git a/src/config/keybindings/actions.rs b/src/config/keybindings/actions.rs index ea81c171..9a55c86b 100644 --- a/src/config/keybindings/actions.rs +++ b/src/config/keybindings/actions.rs @@ -43,6 +43,7 @@ pub enum Action { DecreaseMarkerOpacity, SelectSelectionTool, SelectMarkerTool, + SelectStepMarkerTool, SelectEraserTool, ToggleEraserMode, SelectPenTool, @@ -54,6 +55,7 @@ pub enum Action { IncreaseFontSize, DecreaseFontSize, ResetArrowLabelCounter, + ResetStepMarkerCounter, // Board mode toggles ToggleWhiteboard, diff --git a/src/config/keybindings/config/map/tools.rs b/src/config/keybindings/config/map/tools.rs index 870b978d..0ac10fe4 100644 --- a/src/config/keybindings/config/map/tools.rs +++ b/src/config/keybindings/config/map/tools.rs @@ -22,6 +22,10 @@ impl KeybindingsConfig { Action::SelectSelectionTool, )?; inserter.insert_all(&self.tools.select_marker_tool, Action::SelectMarkerTool)?; + inserter.insert_all( + &self.tools.select_step_marker_tool, + Action::SelectStepMarkerTool, + )?; inserter.insert_all(&self.tools.select_eraser_tool, Action::SelectEraserTool)?; inserter.insert_all(&self.tools.toggle_eraser_mode, Action::ToggleEraserMode)?; inserter.insert_all(&self.tools.select_pen_tool, Action::SelectPenTool)?; @@ -43,6 +47,10 @@ impl KeybindingsConfig { &self.tools.reset_arrow_labels, Action::ResetArrowLabelCounter, )?; + inserter.insert_all( + &self.tools.reset_step_markers, + Action::ResetStepMarkerCounter, + )?; Ok(()) } } diff --git a/src/config/keybindings/config/types/bindings/tools.rs b/src/config/keybindings/config/types/bindings/tools.rs index bfbc90fb..ebf375ab 100644 --- a/src/config/keybindings/config/types/bindings/tools.rs +++ b/src/config/keybindings/config/types/bindings/tools.rs @@ -23,6 +23,9 @@ pub struct ToolKeybindingsConfig { #[serde(default = "default_select_marker_tool")] pub select_marker_tool: Vec, + #[serde(default = "default_select_step_marker_tool")] + pub select_step_marker_tool: Vec, + #[serde(default = "default_select_eraser_tool")] pub select_eraser_tool: Vec, @@ -58,6 +61,9 @@ pub struct ToolKeybindingsConfig { #[serde(default = "default_reset_arrow_labels")] pub reset_arrow_labels: Vec, + + #[serde(default = "default_reset_step_markers")] + pub reset_step_markers: Vec, } impl Default for ToolKeybindingsConfig { @@ -69,6 +75,7 @@ impl Default for ToolKeybindingsConfig { decrease_marker_opacity: default_decrease_marker_opacity(), select_selection_tool: default_select_selection_tool(), select_marker_tool: default_select_marker_tool(), + select_step_marker_tool: default_select_step_marker_tool(), select_eraser_tool: default_select_eraser_tool(), toggle_eraser_mode: default_toggle_eraser_mode(), select_pen_tool: default_select_pen_tool(), @@ -81,6 +88,7 @@ impl Default for ToolKeybindingsConfig { increase_font_size: default_increase_font_size(), decrease_font_size: default_decrease_font_size(), reset_arrow_labels: default_reset_arrow_labels(), + reset_step_markers: default_reset_step_markers(), } } } diff --git a/src/config/keybindings/defaults/tools.rs b/src/config/keybindings/defaults/tools.rs index cf3d7d5b..c0944840 100644 --- a/src/config/keybindings/defaults/tools.rs +++ b/src/config/keybindings/defaults/tools.rs @@ -22,6 +22,10 @@ pub(crate) fn default_select_marker_tool() -> Vec { vec!["H".to_string()] } +pub(crate) fn default_select_step_marker_tool() -> Vec { + Vec::new() +} + pub(crate) fn default_select_eraser_tool() -> Vec { vec!["D".to_string()] } @@ -69,3 +73,7 @@ pub(crate) fn default_decrease_font_size() -> Vec { pub(crate) fn default_reset_arrow_labels() -> Vec { vec!["Ctrl+Shift+R".to_string()] } + +pub(crate) fn default_reset_step_markers() -> Vec { + Vec::new() +} diff --git a/src/draw/mod.rs b/src/draw/mod.rs index 97284c30..a9804c2e 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -30,7 +30,9 @@ pub use render::{ render_text, }; #[allow(unused_imports)] -pub use shape::{ArrowLabel, EraserBrush, EraserKind, Shape, invalidate_text_cache}; +pub use shape::{ + ArrowLabel, EraserBrush, EraserKind, Shape, StepMarkerLabel, invalidate_text_cache, +}; // Re-export color constants for public API (unused internally but part of public interface) #[allow(unused_imports)] diff --git a/src/draw/render/selection.rs b/src/draw/render/selection.rs index ade9ff7a..11e86153 100644 --- a/src/draw/render/selection.rs +++ b/src/draw/render/selection.rs @@ -1,6 +1,8 @@ +use super::highlight::render_click_highlight; use super::primitives::{render_arrow, render_ellipse, render_line, render_rect}; use super::strokes::render_freehand_borrowed; use crate::draw::frame::DrawnShape; +use crate::draw::shape::{step_marker_outline_thickness, step_marker_radius}; use crate::draw::{Color, Shape}; /// Renders a selection halo overlay for a drawn shape. @@ -90,6 +92,25 @@ pub fn render_selection_halo(ctx: &cairo::Context, drawn: &DrawnShape) { Shape::MarkerStroke { points, thick, .. } => { render_freehand_borrowed(ctx, points, glow, thick + outline_width); } + Shape::StepMarker { x, y, label, .. } => { + let radius = step_marker_radius(label.value, label.size, &label.font_descriptor); + let outline = step_marker_outline_thickness(label.size); + let halo_radius = radius + outline_width; + let fill = Color { + a: glow.a * 0.4, + ..glow + }; + render_click_highlight( + ctx, + *x as f64, + *y as f64, + halo_radius, + outline + outline_width, + fill, + glow, + 1.0, + ); + } Shape::EraserStroke { points, brush } => { let outline = brush.size + outline_width; render_freehand_borrowed(ctx, points, glow, outline); diff --git a/src/draw/render/shapes.rs b/src/draw/render/shapes.rs index 13c051e6..c8b6f60a 100644 --- a/src/draw/render/shapes.rs +++ b/src/draw/render/shapes.rs @@ -1,10 +1,15 @@ +use super::highlight::render_click_highlight; use super::primitives::{render_arrow, render_ellipse, render_line, render_rect}; use super::strokes::{ render_freehand_borrowed, render_freehand_pressure_borrowed, render_marker_stroke_borrowed, }; use super::text::{render_sticky_note, render_text}; use crate::draw::shape::Shape; -use crate::draw::shape::{ARROW_LABEL_BACKGROUND, arrow_label_layout}; +use crate::draw::shape::{ + ARROW_LABEL_BACKGROUND, arrow_label_layout, measure_text_with_context, + step_marker_outline_thickness, step_marker_radius, +}; +use crate::draw::Color; /// Renders a single shape to a Cairo context. /// @@ -137,6 +142,77 @@ pub fn render_shape(ctx: &cairo::Context, shape: &Shape) { *wrap_width, ); } + Shape::StepMarker { x, y, color, label } => { + let label_text = label.value.to_string(); + let radius = step_marker_radius(label.value, label.size, &label.font_descriptor); + let outline_thickness = step_marker_outline_thickness(label.size); + let fill_color = Color { + a: (color.a * 0.9).clamp(0.65, 1.0), + ..*color + }; + let brightness = fill_color.r * 0.299 + fill_color.g * 0.587 + fill_color.b * 0.114; + let (outline_color, text_color) = if brightness > 0.6 { + ( + Color { + r: 0.05, + g: 0.05, + b: 0.05, + a: 0.85, + }, + Color { + r: 0.12, + g: 0.12, + b: 0.12, + a: 1.0, + }, + ) + } else { + ( + Color { + r: 0.98, + g: 0.98, + b: 0.98, + a: 0.9, + }, + Color { + r: 0.98, + g: 0.98, + b: 0.98, + a: 1.0, + }, + ) + }; + render_click_highlight( + ctx, + *x as f64, + *y as f64, + radius, + outline_thickness, + fill_color, + outline_color, + 1.0, + ); + let font_desc = label.font_descriptor.to_pango_string(label.size); + if let Some(metrics) = + measure_text_with_context(ctx, &label_text, &font_desc, label.size, None) + { + let center_offset_x = metrics.ink_x + metrics.ink_width / 2.0; + let center_offset_y = metrics.ink_y + metrics.ink_height / 2.0; + let baseline_x = (*x as f64 - center_offset_x).round() as i32; + let baseline_y = (*y as f64 - center_offset_y + metrics.baseline).round() as i32; + render_text( + ctx, + baseline_x, + baseline_y, + &label_text, + text_color, + label.size, + &label.font_descriptor, + false, + None, + ); + } + } Shape::StickyNote { x, y, diff --git a/src/draw/shape/mod.rs b/src/draw/shape/mod.rs index 86f42cf7..49349817 100644 --- a/src/draw/shape/mod.rs +++ b/src/draw/shape/mod.rs @@ -2,18 +2,20 @@ mod arrow_label; mod bounds; +mod step_marker; mod text; mod text_cache; mod types; pub use text_cache::invalidate_text_cache; -pub use types::{ArrowLabel, EraserBrush, EraserKind, Shape}; +pub use types::{ArrowLabel, EraserBrush, EraserKind, Shape, StepMarkerLabel}; pub(crate) use arrow_label::{ARROW_LABEL_BACKGROUND, arrow_label_layout}; pub(crate) use bounds::{ bounding_box_for_arrow, bounding_box_for_ellipse, bounding_box_for_eraser, bounding_box_for_line, bounding_box_for_points, bounding_box_for_rect, }; +pub(crate) use step_marker::{step_marker_bounds, step_marker_outline_thickness, step_marker_radius}; pub(crate) use text::{ bounding_box_for_sticky_note, bounding_box_for_text, sticky_note_layout, sticky_note_text_layout, diff --git a/src/draw/shape/step_marker.rs b/src/draw/shape/step_marker.rs new file mode 100644 index 00000000..6d5be92b --- /dev/null +++ b/src/draw/shape/step_marker.rs @@ -0,0 +1,43 @@ +use crate::draw::FontDescriptor; +use crate::util::Rect; + +use super::text_cache::measure_text_cached; + +const STEP_MARKER_PADDING_RATIO: f64 = 0.45; +const STEP_MARKER_PADDING_MIN: f64 = 6.0; +const STEP_MARKER_MIN_RADIUS: f64 = 10.0; + +pub(crate) fn step_marker_radius( + value: u32, + size: f64, + font_descriptor: &FontDescriptor, +) -> f64 { + let text = value.to_string(); + let font_desc_str = font_descriptor.to_pango_string(size); + let max_dim = measure_text_cached(&text, &font_desc_str, size, None) + .map(|m| m.ink_width.max(m.ink_height)) + .unwrap_or(size * 0.6); + let padding = (size * STEP_MARKER_PADDING_RATIO).max(STEP_MARKER_PADDING_MIN); + (max_dim / 2.0 + padding).max(STEP_MARKER_MIN_RADIUS) +} + +pub(crate) fn step_marker_outline_thickness(size: f64) -> f64 { + (size * 0.12).max(1.5) +} + +pub(crate) fn step_marker_bounds( + x: i32, + y: i32, + value: u32, + size: f64, + font_descriptor: &FontDescriptor, +) -> Option { + let radius = step_marker_radius(value, size, font_descriptor); + let outline = step_marker_outline_thickness(size); + let total = radius + (outline / 2.0); + let min_x = (x as f64 - total).floor() as i32; + let max_x = (x as f64 + total).ceil() as i32; + let min_y = (y as f64 - total).floor() as i32; + let max_y = (y as f64 + total).ceil() as i32; + Rect::from_min_max(min_x, min_y, max_x, max_y) +} diff --git a/src/draw/shape/types.rs b/src/draw/shape/types.rs index 8c2e9892..6f34ca29 100644 --- a/src/draw/shape/types.rs +++ b/src/draw/shape/types.rs @@ -2,6 +2,7 @@ use super::bounds::{ bounding_box_for_arrow, bounding_box_for_ellipse, bounding_box_for_eraser, bounding_box_for_line, bounding_box_for_points, bounding_box_for_rect, }; +use super::step_marker::step_marker_bounds; use super::text::{bounding_box_for_sticky_note, bounding_box_for_text}; use crate::draw::color::Color; use crate::draw::font::FontDescriptor; @@ -36,6 +37,17 @@ pub struct ArrowLabel { pub font_descriptor: FontDescriptor, } +/// Label metadata for numbered step markers. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StepMarkerLabel { + /// Numeric label value. + pub value: u32, + /// Font size in points. + pub size: f64, + /// Font descriptor (family, weight, style). + pub font_descriptor: FontDescriptor, +} + /// Represents a drawable shape or annotation on screen. /// /// Each variant represents a different drawing tool/primitive with its specific parameters. @@ -132,6 +144,17 @@ pub enum Shape { #[serde(default, skip_serializing_if = "Option::is_none")] label: Option, }, + /// Numbered step marker bubble. + StepMarker { + /// Center X coordinate + x: i32, + /// Center Y coordinate + y: i32, + /// Fill color for the marker bubble + color: Color, + /// Label metadata (number + font) + label: StepMarkerLabel, + }, /// Text annotation (activated with 'T' key) Text { /// Baseline X coordinate @@ -281,6 +304,9 @@ impl Shape { *background_enabled, *wrap_width, ), + Shape::StepMarker { x, y, label, .. } => { + step_marker_bounds(*x, *y, label.value, label.size, &label.font_descriptor) + } Shape::StickyNote { x, y, @@ -309,6 +335,7 @@ impl Shape { Shape::Text { .. } => "Text", Shape::StickyNote { .. } => "Sticky Note", Shape::MarkerStroke { .. } => "Marker", + Shape::StepMarker { .. } => "Step Marker", Shape::EraserStroke { .. } => "Eraser", } } diff --git a/src/input/hit_test/mod.rs b/src/input/hit_test/mod.rs index d04ff716..2ee814e8 100644 --- a/src/input/hit_test/mod.rs +++ b/src/input/hit_test/mod.rs @@ -6,7 +6,9 @@ mod shapes; #[cfg(test)] mod tests; -use crate::draw::shape::arrow_label_layout; +use crate::draw::shape::{ + arrow_label_layout, step_marker_outline_thickness, step_marker_radius, +}; use crate::draw::{DrawnShape, Shape}; use crate::util::Rect; @@ -131,6 +133,11 @@ pub fn hit_test(shape: &DrawnShape, point: (i32, i32), tolerance: f64) -> bool { let effective_thick = (*thick * 1.35).max(*thick + 1.0); shapes::freehand_hit(points, point, effective_thick, tolerance) } + Shape::StepMarker { x, y, label, .. } => { + let radius = step_marker_radius(label.value, label.size, &label.font_descriptor); + let outline = step_marker_outline_thickness(label.size); + shapes::circle_hit(*x, *y, radius + outline / 2.0, point, tolerance) + } Shape::EraserStroke { .. } => false, } } diff --git a/src/input/hit_test/shapes.rs b/src/input/hit_test/shapes.rs index 8731b541..104e2fe4 100644 --- a/src/input/hit_test/shapes.rs +++ b/src/input/hit_test/shapes.rs @@ -119,6 +119,19 @@ pub(super) fn ellipse_outline_hit( outer && !inner } +pub(super) fn circle_hit( + cx: i32, + cy: i32, + radius: f64, + point: (i32, i32), + tolerance: f64, +) -> bool { + let dx = point.0 as f64 - cx as f64; + let dy = point.1 as f64 - cy as f64; + let r = radius + tolerance.max(0.5); + (dx * dx + dy * dy) <= r * r +} + #[allow(clippy::too_many_arguments)] pub(super) fn arrowhead_hit( tip_x: i32, diff --git a/src/input/state/actions/action_tools.rs b/src/input/state/actions/action_tools.rs index 43ced145..310d3c08 100644 --- a/src/input/state/actions/action_tools.rs +++ b/src/input/state/actions/action_tools.rs @@ -49,6 +49,9 @@ impl InputState { Action::SelectMarkerTool => { self.set_tool_override(Some(Tool::Marker)); } + Action::SelectStepMarkerTool => { + self.set_tool_override(Some(Tool::StepMarker)); + } Action::SelectEraserTool => { self.set_tool_override(Some(Tool::Eraser)); } @@ -87,6 +90,11 @@ impl InputState { info!("Arrow label counter reset"); } } + Action::ResetStepMarkerCounter => { + if self.reset_step_marker_counter() { + info!("Step marker counter reset"); + } + } Action::ToggleFill => { let enable = !self.fill_enabled; if self.set_fill_enabled(enable) { diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index 6b0f90bd..eca902e0 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -84,6 +84,7 @@ impl InputState { arrow_head_at_end, arrow_label_enabled: false, arrow_label_counter: 1, + step_marker_counter: 1, modifiers: Modifiers::new(), state: DrawingState::Idle, should_exit: false, diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index 306e488c..f24509a6 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -81,6 +81,8 @@ pub struct InputState { pub arrow_label_enabled: bool, /// Next label value for auto-numbered arrows pub arrow_label_counter: u32, + /// Next label value for step markers + pub step_marker_counter: u32, /// Current modifier key state pub modifiers: Modifiers, /// Current drawing mode state machine diff --git a/src/input/state/core/dirty.rs b/src/input/state/core/dirty.rs index 2ce48809..d853e094 100644 --- a/src/input/state/core/dirty.rs +++ b/src/input/state/core/dirty.rs @@ -2,7 +2,7 @@ use super::base::{DrawingState, InputState, TextInputMode}; use crate::draw::shape::{ bounding_box_for_arrow, bounding_box_for_ellipse, bounding_box_for_eraser, bounding_box_for_line, bounding_box_for_points, bounding_box_for_rect, - bounding_box_for_sticky_note, bounding_box_for_text, + bounding_box_for_sticky_note, bounding_box_for_text, step_marker_bounds, }; use crate::input::tool::Tool; use crate::util::{self, Rect}; @@ -101,6 +101,16 @@ impl InputState { label.as_ref(), ) } + Tool::StepMarker => { + let label = self.next_step_marker_label(); + step_marker_bounds( + current_x, + current_y, + label.value, + label.size, + &label.font_descriptor, + ) + } Tool::Highlight => None, Tool::Select => None, }, diff --git a/src/input/state/core/properties/apply_selection/actions/color.rs b/src/input/state/core/properties/apply_selection/actions/color.rs index 13a6955d..86d0720f 100644 --- a/src/input/state/core/properties/apply_selection/actions/color.rs +++ b/src/input/state/core/properties/apply_selection/actions/color.rs @@ -27,6 +27,7 @@ impl InputState { | Shape::Arrow { .. } | Shape::MarkerStroke { .. } | Shape::Text { .. } + | Shape::StepMarker { .. } | Shape::StickyNote { .. } ) }, @@ -37,7 +38,8 @@ impl InputState { | Shape::Rect { color, .. } | Shape::Ellipse { color, .. } | Shape::Arrow { color, .. } - | Shape::Text { color, .. } => { + | Shape::Text { color, .. } + | Shape::StepMarker { color, .. } => { if *color != target { *color = target; true diff --git a/src/input/state/core/properties/summary.rs b/src/input/state/core/properties/summary.rs index c4ef70f9..12f39175 100644 --- a/src/input/state/core/properties/summary.rs +++ b/src/input/state/core/properties/summary.rs @@ -72,7 +72,8 @@ pub(super) fn shape_color(shape: &Shape) -> Option { | Shape::Rect { color, .. } | Shape::Ellipse { color, .. } | Shape::Arrow { color, .. } - | Shape::Text { color, .. } => Some(*color), + | Shape::Text { color, .. } + | Shape::StepMarker { color, .. } => Some(*color), Shape::MarkerStroke { color, .. } => Some(Color { a: 1.0, ..*color }), Shape::StickyNote { background, .. } => Some(*background), _ => None, diff --git a/src/input/state/core/selection_actions/translation/transform.rs b/src/input/state/core/selection_actions/translation/transform.rs index 0606fab5..ae385ae6 100644 --- a/src/input/state/core/selection_actions/translation/transform.rs +++ b/src/input/state/core/selection_actions/translation/transform.rs @@ -75,6 +75,10 @@ impl InputState { *x += dx; *y += dy; } + Shape::StepMarker { x, y, .. } => { + *x += dx; + *y += dy; + } Shape::StickyNote { x, y, .. } => { *x += dx; *y += dy; diff --git a/src/input/state/core/utility/mod.rs b/src/input/state/core/utility/mod.rs index 9115c49b..e3e13404 100644 --- a/src/input/state/core/utility/mod.rs +++ b/src/input/state/core/utility/mod.rs @@ -1,5 +1,6 @@ mod actions; mod arrow_labels; +mod step_markers; mod font; mod frozen_zoom; mod help_overlay; diff --git a/src/input/state/core/utility/step_markers.rs b/src/input/state/core/utility/step_markers.rs new file mode 100644 index 00000000..949436cb --- /dev/null +++ b/src/input/state/core/utility/step_markers.rs @@ -0,0 +1,49 @@ +use super::super::base::InputState; +use crate::draw::{Shape, StepMarkerLabel}; + +const STEP_MARKER_FONT_SCALE: f64 = 0.6; +const STEP_MARKER_MIN_SIZE: f64 = 12.0; +const STEP_MARKER_MAX_SIZE: f64 = 36.0; + +impl InputState { + pub(crate) fn next_step_marker_label(&self) -> StepMarkerLabel { + StepMarkerLabel { + value: self.step_marker_counter.max(1), + size: self.step_marker_size(), + font_descriptor: self.font_descriptor.clone(), + } + } + + pub(crate) fn bump_step_marker(&mut self) { + self.step_marker_counter = self.step_marker_counter.saturating_add(1); + } + + pub(crate) fn reset_step_marker_counter(&mut self) -> bool { + if self.step_marker_counter == 1 { + return false; + } + self.step_marker_counter = 1; + self.dirty_tracker.mark_full(); + self.needs_redraw = true; + true + } + + pub(crate) fn sync_step_marker_counter(&mut self) { + let mut max_label = 0; + for board in self.boards.board_states() { + for frame in board.pages.pages() { + for drawn in &frame.shapes { + if let Shape::StepMarker { label, .. } = &drawn.shape { + max_label = max_label.max(label.value); + } + } + } + } + self.step_marker_counter = max_label.saturating_add(1); + } + + fn step_marker_size(&self) -> f64 { + (self.current_font_size * STEP_MARKER_FONT_SCALE) + .clamp(STEP_MARKER_MIN_SIZE, STEP_MARKER_MAX_SIZE) + } +} diff --git a/src/input/state/mouse/release/drawing.rs b/src/input/state/mouse/release/drawing.rs index 055fdc21..dad76cdc 100644 --- a/src/input/state/mouse/release/drawing.rs +++ b/src/input/state/mouse/release/drawing.rs @@ -27,6 +27,12 @@ pub(super) fn finish_drawing(state: &mut InputState, tool: Tool, release: Drawin None }; let used_arrow_label = label.is_some(); + let step_label = if matches!(tool, Tool::StepMarker) { + Some(state.next_step_marker_label()) + } else { + None + }; + let used_step_marker = step_label.is_some(); let shape = match tool { Tool::Pen => { // Check if we have pressure data and if it varies enough to matter @@ -119,6 +125,12 @@ pub(super) fn finish_drawing(state: &mut InputState, tool: Tool, release: Drawin color: state.marker_color(), thick: state.current_thickness, }, + Tool::StepMarker => Shape::StepMarker { + x: end_x, + y: end_y, + color: state.current_color, + label: step_label.expect("step label required"), + }, Tool::Eraser => { if state.eraser_mode == EraserMode::Stroke { state.clear_provisional_dirty(); @@ -190,6 +202,9 @@ pub(super) fn finish_drawing(state: &mut InputState, tool: Tool, release: Drawin if used_arrow_label { state.bump_arrow_label(); } + if used_step_marker { + state.bump_step_marker(); + } } else if limit_reached { warn!( "Shape limit ({}) reached; discarding new shape", diff --git a/src/input/state/render.rs b/src/input/state/render.rs index d09cd55a..79ded188 100644 --- a/src/input/state/render.rs +++ b/src/input/state/render.rs @@ -80,6 +80,12 @@ impl InputState { head_at_end: self.arrow_head_at_end, label: self.next_arrow_label(), }), + Tool::StepMarker => Some(Shape::StepMarker { + x: current_x, + y: current_y, + color: self.current_color, + label: self.next_step_marker_label(), + }), } } else { None diff --git a/src/input/state/tests/mod.rs b/src/input/state/tests/mod.rs index fa0c8093..724a2335 100644 --- a/src/input/state/tests/mod.rs +++ b/src/input/state/tests/mod.rs @@ -13,6 +13,7 @@ mod basics; mod board_picker; mod drawing; mod erase; +mod step_markers; mod menus; mod presenter_mode; mod pressure_modes; diff --git a/src/input/state/tests/step_markers.rs b/src/input/state/tests/step_markers.rs new file mode 100644 index 00000000..74b6a633 --- /dev/null +++ b/src/input/state/tests/step_markers.rs @@ -0,0 +1,48 @@ +use super::*; + +use crate::draw::StepMarkerLabel; +use crate::input::{BOARD_ID_BLACKBOARD, BOARD_ID_WHITEBOARD}; + +fn step_marker_with_label(value: u32, font_descriptor: &FontDescriptor) -> Shape { + Shape::StepMarker { + x: 10, + y: 20, + color: Color { + r: 0.2, + g: 0.4, + b: 0.8, + a: 1.0, + }, + label: StepMarkerLabel { + value, + size: 14.0, + font_descriptor: font_descriptor.clone(), + }, + } +} + +#[test] +fn sync_step_marker_counter_uses_max_across_boards() { + let mut state = create_test_input_state(); + let font_descriptor = state.font_descriptor.clone(); + + state + .boards + .active_frame_mut() + .add_shape(step_marker_with_label(3, &font_descriptor)); + + state.switch_board(BOARD_ID_WHITEBOARD); + state + .boards + .active_frame_mut() + .add_shape(step_marker_with_label(9, &font_descriptor)); + + state.switch_board(BOARD_ID_BLACKBOARD); + state + .boards + .active_frame_mut() + .add_shape(step_marker_with_label(5, &font_descriptor)); + + state.sync_step_marker_counter(); + assert_eq!(state.step_marker_counter, 10); +} diff --git a/src/input/tool.rs b/src/input/tool.rs index ca4e86e2..5202e7ac 100644 --- a/src/input/tool.rs +++ b/src/input/tool.rs @@ -26,6 +26,8 @@ pub enum Tool { Marker, /// Highlight-only tool (no drawing, emits click highlight) Highlight, + /// Numbered step marker tool (places auto-incrementing bubbles) + StepMarker, /// Eraser brush that removes content within its stroke Eraser, // Note: Text mode uses DrawingState::TextInput instead of Tool::Text diff --git a/src/session/snapshot/apply.rs b/src/session/snapshot/apply.rs index c6c25041..48f48a6d 100644 --- a/src/session/snapshot/apply.rs +++ b/src/session/snapshot/apply.rs @@ -82,6 +82,7 @@ pub fn apply_snapshot(input: &mut InputState, snapshot: SessionSnapshot, options } } + input.sync_step_marker_counter(); input.needs_redraw = true; } diff --git a/src/toolbar_icons/tools.rs b/src/toolbar_icons/tools.rs index b45c3ed2..0fde031d 100644 --- a/src/toolbar_icons/tools.rs +++ b/src/toolbar_icons/tools.rs @@ -264,3 +264,36 @@ pub fn draw_icon_marker(ctx: &Context, x: f64, y: f64, size: f64) { let _ = ctx.fill(); ctx.restore().ok(); } + +/// Draw a step marker icon (numbered circle) +pub fn draw_icon_step_marker(ctx: &Context, x: f64, y: f64, size: f64) { + let s = size; + let stroke = (s * 0.1).max(1.6); + ctx.set_line_width(stroke); + ctx.set_line_cap(cairo::LineCap::Round); + ctx.set_line_join(cairo::LineJoin::Round); + + let cx = x + s * 0.5; + let cy = y + s * 0.5; + let r = s * 0.32; + // Offset bubble to hint at a sequence + ctx.arc(cx - r * 0.45, cy - r * 0.45, r * 0.55, 0.0, PI * 2.0); + let _ = ctx.stroke(); + ctx.arc(cx, cy, r, 0.0, PI * 2.0); + let _ = ctx.stroke(); + + // Stylized "1" glyph + let one_h = r * 1.15; + let one_w = r * 0.35; + ctx.set_line_width((s * 0.12).max(1.8)); + ctx.move_to(cx - one_w * 0.3, cy - one_h * 0.45); + ctx.line_to(cx, cy - one_h * 0.6); + ctx.line_to(cx + one_w * 0.25, cy - one_h * 0.35); + let _ = ctx.stroke(); + ctx.move_to(cx, cy - one_h * 0.45); + ctx.line_to(cx, cy + one_h * 0.5); + let _ = ctx.stroke(); + ctx.move_to(cx - one_w, cy + one_h * 0.5); + ctx.line_to(cx + one_w, cy + one_h * 0.5); + let _ = ctx.stroke(); +} diff --git a/src/ui/help_overlay/sections/builder.rs b/src/ui/help_overlay/sections/builder.rs index e4508934..a73b3d29 100644 --- a/src/ui/help_overlay/sections/builder.rs +++ b/src/ui/help_overlay/sections/builder.rs @@ -163,6 +163,10 @@ pub(crate) fn build_section_sets( binding_or_fallback(bindings, Action::SelectMarkerTool, NOT_BOUND_LABEL), action_label(Action::SelectMarkerTool), ), + row( + binding_or_fallback(bindings, Action::SelectStepMarkerTool, NOT_BOUND_LABEL), + action_label(Action::SelectStepMarkerTool), + ), row( binding_or_fallback(bindings, Action::SelectEraserTool, NOT_BOUND_LABEL), action_label(Action::SelectEraserTool), diff --git a/src/ui/toolbar/apply/mod.rs b/src/ui/toolbar/apply/mod.rs index b72d4a6a..806a793a 100644 --- a/src/ui/toolbar/apply/mod.rs +++ b/src/ui/toolbar/apply/mod.rs @@ -27,6 +27,9 @@ impl InputState { self.apply_toolbar_toggle_arrow_labels(enable) } ToolbarEvent::ResetArrowLabelCounter => self.apply_toolbar_reset_arrow_label_counter(), + ToolbarEvent::ResetStepMarkerCounter => { + self.apply_toolbar_reset_step_marker_counter() + } ToolbarEvent::SetUndoDelay(delay_secs) => self.apply_toolbar_set_undo_delay(delay_secs), ToolbarEvent::SetRedoDelay(delay_secs) => self.apply_toolbar_set_redo_delay(delay_secs), ToolbarEvent::SetCustomUndoDelay(delay_secs) => { diff --git a/src/ui/toolbar/apply/tools.rs b/src/ui/toolbar/apply/tools.rs index bd5936ca..6574a99e 100644 --- a/src/ui/toolbar/apply/tools.rs +++ b/src/ui/toolbar/apply/tools.rs @@ -51,6 +51,10 @@ impl InputState { self.reset_arrow_label_counter() } + pub(super) fn apply_toolbar_reset_step_marker_counter(&mut self) -> bool { + self.reset_step_marker_counter() + } + pub(super) fn apply_toolbar_nudge_thickness(&mut self, delta: f64) -> bool { self.nudge_thickness_for_active_tool(delta) } diff --git a/src/ui/toolbar/bindings.rs b/src/ui/toolbar/bindings.rs index 65224a78..e33a8c27 100644 --- a/src/ui/toolbar/bindings.rs +++ b/src/ui/toolbar/bindings.rs @@ -57,6 +57,7 @@ pub(crate) fn action_for_tool(tool: Tool) -> Option { Tool::Ellipse => Some(Action::SelectEllipseTool), Tool::Arrow => Some(Action::SelectArrowTool), Tool::Marker => Some(Action::SelectMarkerTool), + Tool::StepMarker => Some(Action::SelectStepMarkerTool), Tool::Highlight => Some(Action::SelectHighlightTool), Tool::Eraser => Some(Action::SelectEraserTool), } @@ -104,6 +105,7 @@ pub(crate) fn action_for_event(event: &ToolbarEvent) -> Option { ToolbarEvent::ZoomIn => Some(Action::ZoomIn), ToolbarEvent::ZoomOut => Some(Action::ZoomOut), ToolbarEvent::ResetZoom => Some(Action::ResetZoom), + ToolbarEvent::ResetStepMarkerCounter => Some(Action::ResetStepMarkerCounter), ToolbarEvent::ToggleZoomLock => Some(Action::ToggleZoomLock), ToolbarEvent::ApplyPreset(slot) => action_for_apply_preset(*slot), ToolbarEvent::SavePreset(slot) => action_for_save_preset(*slot), diff --git a/src/ui/toolbar/events.rs b/src/ui/toolbar/events.rs index 1a13e9c7..068558f2 100644 --- a/src/ui/toolbar/events.rs +++ b/src/ui/toolbar/events.rs @@ -17,6 +17,7 @@ pub enum ToolbarEvent { ToggleFill(bool), ToggleArrowLabels(bool), ResetArrowLabelCounter, + ResetStepMarkerCounter, SetUndoDelay(f64), SetRedoDelay(f64), UndoAll, diff --git a/src/ui/toolbar/snapshot.rs b/src/ui/toolbar/snapshot.rs index 6f3a80a8..3d607fe3 100644 --- a/src/ui/toolbar/snapshot.rs +++ b/src/ui/toolbar/snapshot.rs @@ -56,6 +56,7 @@ pub struct ToolbarSnapshot { pub fill_enabled: bool, pub arrow_label_enabled: bool, pub arrow_label_next: u32, + pub step_marker_next: u32, pub undo_available: bool, pub redo_available: bool, pub board_index: usize, @@ -255,6 +256,7 @@ impl ToolbarSnapshot { fill_enabled: state.fill_enabled, arrow_label_enabled: state.arrow_label_enabled, arrow_label_next: state.arrow_label_counter.max(1), + step_marker_next: state.step_marker_counter.max(1), undo_available: frame.undo_stack_len() > 0, redo_available: frame.redo_stack_len() > 0, board_index, From 7e102ab4dfdfd7ecebee91fca8b1186c5fb9b7ca Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:37:36 +0100 Subject: [PATCH 2/4] fix: widen top toolbar for step marker tool --- .../wayland/toolbar/layout/spec/top.rs | 10 +-- .../wayland/toolbar/layout/tests/mod.rs | 4 +- src/toolbar_icons/tools.rs | 69 ++++++++++++++----- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/backend/wayland/toolbar/layout/spec/top.rs b/src/backend/wayland/toolbar/layout/spec/top.rs index 85e692b6..add40484 100644 --- a/src/backend/wayland/toolbar/layout/spec/top.rs +++ b/src/backend/wayland/toolbar/layout/spec/top.rs @@ -8,8 +8,8 @@ impl ToolbarLayoutSpec { pub(in crate::backend::wayland::toolbar) const TOP_SIZE_ICONS: (u32, u32) = (735, 80); pub(in crate::backend::wayland::toolbar) const TOP_SIZE_TEXT: (u32, u32) = (875, 56); - pub(in crate::backend::wayland::toolbar) const TOP_GAP: f64 = 8.0; - pub(in crate::backend::wayland::toolbar) const TOP_START_X: f64 = 16.0; + pub(in crate::backend::wayland::toolbar) const TOP_GAP: f64 = 10.0; + pub(in crate::backend::wayland::toolbar) const TOP_START_X: f64 = 28.0; pub(in crate::backend::wayland::toolbar) const TOP_HANDLE_SIZE: f64 = 18.0; pub(in crate::backend::wayland::toolbar) const TOP_HANDLE_Y: f64 = 10.0; pub(in crate::backend::wayland::toolbar) const TOP_ICON_BUTTON: f64 = 44.0; @@ -23,7 +23,7 @@ impl ToolbarLayoutSpec { pub(in crate::backend::wayland::toolbar) const TOP_TOGGLE_WIDTH: f64 = 84.0; pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_SIZE: f64 = 24.0; pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_MARGIN_RIGHT: f64 = 12.0; + pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_MARGIN_RIGHT: f64 = 24.0; pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_Y_ICON: f64 = 15.0; pub(in crate::backend::wayland::toolbar) const TOP_SHAPE_ROW_GAP: f64 = 6.0; @@ -53,9 +53,9 @@ impl ToolbarLayoutSpec { Self::TOP_TEXT_BUTTON_W }; let tool_count = if self.layout_mode == ToolbarLayoutMode::Simple { - 4 + 5 } else { - 8 + 9 }; let mut x = Self::TOP_START_X + Self::TOP_HANDLE_SIZE + gap; x += tool_count as f64 * (btn_w + gap); diff --git a/src/backend/wayland/toolbar/layout/tests/mod.rs b/src/backend/wayland/toolbar/layout/tests/mod.rs index 00c9a10f..e8f1f9eb 100644 --- a/src/backend/wayland/toolbar/layout/tests/mod.rs +++ b/src/backend/wayland/toolbar/layout/tests/mod.rs @@ -59,11 +59,11 @@ fn top_size_respects_icon_mode() { let mut state = create_test_input_state(); state.toolbar_use_icons = true; let snapshot = snapshot_from_state(&state); - assert_eq!(top_size(&snapshot), (824, 80)); + assert_eq!(top_size(&snapshot), (930, 80)); state.toolbar_use_icons = false; let snapshot = snapshot_from_state(&state); - assert_eq!(top_size(&snapshot), (948, 56)); + assert_eq!(top_size(&snapshot), (1068, 56)); } #[test] diff --git a/src/toolbar_icons/tools.rs b/src/toolbar_icons/tools.rs index 0fde031d..6e0dd113 100644 --- a/src/toolbar_icons/tools.rs +++ b/src/toolbar_icons/tools.rs @@ -265,7 +265,7 @@ pub fn draw_icon_marker(ctx: &Context, x: f64, y: f64, size: f64) { ctx.restore().ok(); } -/// Draw a step marker icon (numbered circle) +/// Draw a step marker icon (numbered list: 1, 2, 3) pub fn draw_icon_step_marker(ctx: &Context, x: f64, y: f64, size: f64) { let s = size; let stroke = (s * 0.1).max(1.6); @@ -273,27 +273,60 @@ pub fn draw_icon_step_marker(ctx: &Context, x: f64, y: f64, size: f64) { ctx.set_line_cap(cairo::LineCap::Round); ctx.set_line_join(cairo::LineJoin::Round); - let cx = x + s * 0.5; - let cy = y + s * 0.5; - let r = s * 0.32; - // Offset bubble to hint at a sequence - ctx.arc(cx - r * 0.45, cy - r * 0.45, r * 0.55, 0.0, PI * 2.0); + // Draw "1" - top left + let one_x = x + s * 0.22; + let one_top = y + s * 0.12; + let one_bot = y + s * 0.38; + let one_w = s * 0.12; + // Serif at top + ctx.move_to(one_x - one_w * 0.6, one_top + s * 0.06); + ctx.line_to(one_x, one_top); let _ = ctx.stroke(); - ctx.arc(cx, cy, r, 0.0, PI * 2.0); + // Vertical stroke + ctx.move_to(one_x, one_top); + ctx.line_to(one_x, one_bot); + let _ = ctx.stroke(); + // Base + ctx.move_to(one_x - one_w, one_bot); + ctx.line_to(one_x + one_w, one_bot); let _ = ctx.stroke(); - // Stylized "1" glyph - let one_h = r * 1.15; - let one_w = r * 0.35; - ctx.set_line_width((s * 0.12).max(1.8)); - ctx.move_to(cx - one_w * 0.3, cy - one_h * 0.45); - ctx.line_to(cx, cy - one_h * 0.6); - ctx.line_to(cx + one_w * 0.25, cy - one_h * 0.35); + // Draw "2" - top right + let two_x = x + s * 0.62; + let two_top = y + s * 0.12; + let two_bot = y + s * 0.38; + let two_w = s * 0.14; + // Top curve of 2 + ctx.arc(two_x, two_top + s * 0.08, two_w, -PI * 0.9, PI * 0.15); let _ = ctx.stroke(); - ctx.move_to(cx, cy - one_h * 0.45); - ctx.line_to(cx, cy + one_h * 0.5); + // Diagonal and base + ctx.move_to(two_x + two_w * 0.9, two_top + s * 0.14); + ctx.line_to(two_x - two_w, two_bot); + ctx.line_to(two_x + two_w, two_bot); let _ = ctx.stroke(); - ctx.move_to(cx - one_w, cy + one_h * 0.5); - ctx.line_to(cx + one_w, cy + one_h * 0.5); + + // Draw "3" - bottom center + let three_x = x + s * 0.5; + let three_top = y + s * 0.54; + let three_bot = y + s * 0.88; + let three_w = s * 0.16; + let three_h = (three_bot - three_top) / 2.0; + // Top arc of 3 + ctx.arc( + three_x, + three_top + three_h * 0.45, + three_w * 0.85, + -PI * 0.85, + PI * 0.35, + ); + let _ = ctx.stroke(); + // Bottom arc of 3 + ctx.arc( + three_x, + three_bot - three_h * 0.45, + three_w * 0.85, + -PI * 0.35, + PI * 0.85, + ); let _ = ctx.stroke(); } From d366ca98da57e99e04bfc92051a5e39d7022c3a9 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Sun, 25 Jan 2026 00:14:24 +0100 Subject: [PATCH 3/4] fix: persist highlight prefs and tighten toolbar spacing --- src/backend/wayland/handlers/keyboard/mod.rs | 16 ++++++++++++++++ src/backend/wayland/state/toolbar/events.rs | 10 +++++++++- src/backend/wayland/toolbar/layout/spec/top.rs | 6 +++--- src/backend/wayland/toolbar/layout/tests/mod.rs | 4 ++-- src/draw/render/shapes.rs | 13 +++++++------ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/backend/wayland/handlers/keyboard/mod.rs b/src/backend/wayland/handlers/keyboard/mod.rs index 9437e272..9d8bdcb2 100644 --- a/src/backend/wayland/handlers/keyboard/mod.rs +++ b/src/backend/wayland/handlers/keyboard/mod.rs @@ -245,6 +245,11 @@ impl WaylandState { self.input_state.font_descriptor.clone(), self.input_state.fill_enabled, ); + let highlight_before = ( + self.input_state.click_highlight_enabled(), + self.input_state.highlight_tool_ring_enabled(), + self.input_state.presenter_mode, + ); self.input_state.on_key_press(key); self.input_state.needs_redraw = true; let prefs_changed = prefs_before.0 != self.input_state.current_color @@ -257,6 +262,17 @@ impl WaylandState { if prefs_changed { self.save_drawing_preferences(); } + let highlight_after = ( + self.input_state.click_highlight_enabled(), + self.input_state.highlight_tool_ring_enabled(), + self.input_state.presenter_mode, + ); + if highlight_before.2 == highlight_after.2 + && (highlight_before.0 != highlight_after.0 + || highlight_before.1 != highlight_after.1) + { + self.save_click_highlight_preferences(); + } #[cfg(tablet)] if (self.input_state.current_thickness - prev_thickness).abs() > f64::EPSILON { diff --git a/src/backend/wayland/state/toolbar/events.rs b/src/backend/wayland/state/toolbar/events.rs index a35d33fe..fa5b089e 100644 --- a/src/backend/wayland/state/toolbar/events.rs +++ b/src/backend/wayland/state/toolbar/events.rs @@ -98,7 +98,10 @@ impl WaylandState { | ToolbarEvent::ToggleFill(_) | ToolbarEvent::ApplyPreset(_) ); - let persist_click_highlight = matches!(event, ToolbarEvent::ToggleHighlightToolRing(_)); + let persist_click_highlight = matches!( + event, + ToolbarEvent::ToggleAllHighlight(_) | ToolbarEvent::ToggleHighlightToolRing(_) + ); if self.input_state.apply_toolbar_event(event) { self.toolbar.mark_dirty(); @@ -206,6 +209,11 @@ impl WaylandState { } pub(in crate::backend::wayland) fn save_click_highlight_preferences(&mut self) { + if !(self.input_state.presenter_mode + && self.input_state.presenter_mode_config.enable_click_highlight) + { + self.config.ui.click_highlight.enabled = self.input_state.click_highlight_enabled(); + } self.config.ui.click_highlight.show_on_highlight_tool = self.input_state.highlight_tool_ring_enabled(); if let Err(err) = self.config.save() { diff --git a/src/backend/wayland/toolbar/layout/spec/top.rs b/src/backend/wayland/toolbar/layout/spec/top.rs index add40484..baefc623 100644 --- a/src/backend/wayland/toolbar/layout/spec/top.rs +++ b/src/backend/wayland/toolbar/layout/spec/top.rs @@ -8,8 +8,8 @@ impl ToolbarLayoutSpec { pub(in crate::backend::wayland::toolbar) const TOP_SIZE_ICONS: (u32, u32) = (735, 80); pub(in crate::backend::wayland::toolbar) const TOP_SIZE_TEXT: (u32, u32) = (875, 56); - pub(in crate::backend::wayland::toolbar) const TOP_GAP: f64 = 10.0; - pub(in crate::backend::wayland::toolbar) const TOP_START_X: f64 = 28.0; + pub(in crate::backend::wayland::toolbar) const TOP_GAP: f64 = 4.0; + pub(in crate::backend::wayland::toolbar) const TOP_START_X: f64 = 19.0; pub(in crate::backend::wayland::toolbar) const TOP_HANDLE_SIZE: f64 = 18.0; pub(in crate::backend::wayland::toolbar) const TOP_HANDLE_Y: f64 = 10.0; pub(in crate::backend::wayland::toolbar) const TOP_ICON_BUTTON: f64 = 44.0; @@ -23,7 +23,7 @@ impl ToolbarLayoutSpec { pub(in crate::backend::wayland::toolbar) const TOP_TOGGLE_WIDTH: f64 = 84.0; pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_SIZE: f64 = 24.0; pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_GAP: f64 = 6.0; - pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_MARGIN_RIGHT: f64 = 24.0; + pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_MARGIN_RIGHT: f64 = 15.0; pub(in crate::backend::wayland::toolbar) const TOP_PIN_BUTTON_Y_ICON: f64 = 15.0; pub(in crate::backend::wayland::toolbar) const TOP_SHAPE_ROW_GAP: f64 = 6.0; diff --git a/src/backend/wayland/toolbar/layout/tests/mod.rs b/src/backend/wayland/toolbar/layout/tests/mod.rs index e8f1f9eb..7c2cacf1 100644 --- a/src/backend/wayland/toolbar/layout/tests/mod.rs +++ b/src/backend/wayland/toolbar/layout/tests/mod.rs @@ -59,11 +59,11 @@ fn top_size_respects_icon_mode() { let mut state = create_test_input_state(); state.toolbar_use_icons = true; let snapshot = snapshot_from_state(&state); - assert_eq!(top_size(&snapshot), (930, 80)); + assert_eq!(top_size(&snapshot), (822, 80)); state.toolbar_use_icons = false; let snapshot = snapshot_from_state(&state); - assert_eq!(top_size(&snapshot), (1068, 56)); + assert_eq!(top_size(&snapshot), (966, 56)); } #[test] diff --git a/src/draw/render/shapes.rs b/src/draw/render/shapes.rs index c8b6f60a..acaf42a0 100644 --- a/src/draw/render/shapes.rs +++ b/src/draw/render/shapes.rs @@ -146,24 +146,25 @@ pub fn render_shape(ctx: &cairo::Context, shape: &Shape) { let label_text = label.value.to_string(); let radius = step_marker_radius(label.value, label.size, &label.font_descriptor); let outline_thickness = step_marker_outline_thickness(label.size); + let alpha = color.a.clamp(0.0, 1.0); let fill_color = Color { - a: (color.a * 0.9).clamp(0.65, 1.0), + a: (alpha * 0.9).clamp(0.0, 1.0), ..*color }; - let brightness = fill_color.r * 0.299 + fill_color.g * 0.587 + fill_color.b * 0.114; + let brightness = color.r * 0.299 + color.g * 0.587 + color.b * 0.114; let (outline_color, text_color) = if brightness > 0.6 { ( Color { r: 0.05, g: 0.05, b: 0.05, - a: 0.85, + a: 0.85 * alpha, }, Color { r: 0.12, g: 0.12, b: 0.12, - a: 1.0, + a: alpha, }, ) } else { @@ -172,13 +173,13 @@ pub fn render_shape(ctx: &cairo::Context, shape: &Shape) { r: 0.98, g: 0.98, b: 0.98, - a: 0.9, + a: 0.9 * alpha, }, Color { r: 0.98, g: 0.98, b: 0.98, - a: 1.0, + a: alpha, }, ) }; From 3244472723ea8b2d88881cc2692226825f979a03 Mon Sep 17 00:00:00 2001 From: devmobasa <4170275+devmobasa@users.noreply.github.com> Date: Sun, 25 Jan 2026 00:24:34 +0100 Subject: [PATCH 4/4] fix: persist highlight prefs and cover step marker reset --- src/backend/wayland/handlers/keyboard/mod.rs | 3 +- src/backend/wayland/state/toolbar/events.rs | 5 +- .../toolbar/render/side_palette/mod.rs | 2 +- .../render/side_palette/step_marker.rs | 14 ++++- .../render/top_strip/icons/tool_row.rs | 10 +++- src/draw/render/shapes.rs | 2 +- src/draw/shape/mod.rs | 4 +- src/draw/shape/step_marker.rs | 6 +-- src/draw/shape/tests.rs | 27 +++++++++- src/input/hit_test/mod.rs | 4 +- src/input/hit_test/shapes.rs | 8 +-- src/input/hit_test/tests.rs | 42 ++++++++++++++- src/input/state/core/utility/mod.rs | 2 +- src/input/state/tests/mod.rs | 2 +- src/input/state/tests/step_markers.rs | 52 +++++++++++++++++++ src/ui/toolbar/apply/mod.rs | 4 +- 16 files changed, 155 insertions(+), 32 deletions(-) diff --git a/src/backend/wayland/handlers/keyboard/mod.rs b/src/backend/wayland/handlers/keyboard/mod.rs index 9d8bdcb2..62e1a8d1 100644 --- a/src/backend/wayland/handlers/keyboard/mod.rs +++ b/src/backend/wayland/handlers/keyboard/mod.rs @@ -268,8 +268,7 @@ impl WaylandState { self.input_state.presenter_mode, ); if highlight_before.2 == highlight_after.2 - && (highlight_before.0 != highlight_after.0 - || highlight_before.1 != highlight_after.1) + && (highlight_before.0 != highlight_after.0 || highlight_before.1 != highlight_after.1) { self.save_click_highlight_preferences(); } diff --git a/src/backend/wayland/state/toolbar/events.rs b/src/backend/wayland/state/toolbar/events.rs index fa5b089e..6e7a75ce 100644 --- a/src/backend/wayland/state/toolbar/events.rs +++ b/src/backend/wayland/state/toolbar/events.rs @@ -210,7 +210,10 @@ impl WaylandState { pub(in crate::backend::wayland) fn save_click_highlight_preferences(&mut self) { if !(self.input_state.presenter_mode - && self.input_state.presenter_mode_config.enable_click_highlight) + && self + .input_state + .presenter_mode_config + .enable_click_highlight) { self.config.ui.click_highlight.enabled = self.input_state.click_highlight_enabled(); } diff --git a/src/backend/wayland/toolbar/render/side_palette/mod.rs b/src/backend/wayland/toolbar/render/side_palette/mod.rs index 53e30f04..ec94505f 100644 --- a/src/backend/wayland/toolbar/render/side_palette/mod.rs +++ b/src/backend/wayland/toolbar/render/side_palette/mod.rs @@ -8,8 +8,8 @@ mod marker; mod pages; mod presets; mod settings; -mod step_marker; mod step; +mod step_marker; mod text; mod thickness; diff --git a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs index e217a71d..631a6e15 100644 --- a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs +++ b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs @@ -1,4 +1,6 @@ -use super::super::widgets::constants::{COLOR_LABEL_HINT, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SMALL, set_color}; +use super::super::widgets::constants::{ + COLOR_LABEL_HINT, FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL, FONT_SIZE_SMALL, set_color, +}; use super::super::widgets::*; use super::SidePaletteLayout; use crate::backend::wayland::toolbar::events::HitKind; @@ -58,7 +60,15 @@ pub(super) fn draw_step_marker_section(layout: &mut SidePaletteLayout, y: &mut f .map(|(hx, hy)| point_in_rect(hx, hy, x, reset_y, content_width, reset_h)) .unwrap_or(false); draw_button(ctx, x, reset_y, content_width, reset_h, false, reset_hover); - draw_label_center(ctx, label_style, x, reset_y, content_width, reset_h, "Reset"); + draw_label_center( + ctx, + label_style, + x, + reset_y, + content_width, + reset_h, + "Reset", + ); hits.push(HitRegion { rect: (x, reset_y, content_width, reset_h), event: ToolbarEvent::ResetStepMarkerCounter, diff --git a/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs b/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs index a24017a4..39c416d7 100644 --- a/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs +++ b/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs @@ -34,7 +34,10 @@ pub(super) fn draw_tool_row( (Tool::Select, toolbar_icons::draw_icon_select as IconFn), (Tool::Pen, toolbar_icons::draw_icon_pen as IconFn), (Tool::Marker, toolbar_icons::draw_icon_marker as IconFn), - (Tool::StepMarker, toolbar_icons::draw_icon_step_marker as IconFn), + ( + Tool::StepMarker, + toolbar_icons::draw_icon_step_marker as IconFn, + ), (Tool::Eraser, toolbar_icons::draw_icon_eraser as IconFn), ] } else { @@ -42,7 +45,10 @@ pub(super) fn draw_tool_row( (Tool::Select, toolbar_icons::draw_icon_select as IconFn), (Tool::Pen, toolbar_icons::draw_icon_pen as IconFn), (Tool::Marker, toolbar_icons::draw_icon_marker as IconFn), - (Tool::StepMarker, toolbar_icons::draw_icon_step_marker as IconFn), + ( + Tool::StepMarker, + toolbar_icons::draw_icon_step_marker as IconFn, + ), (Tool::Eraser, toolbar_icons::draw_icon_eraser as IconFn), (Tool::Line, toolbar_icons::draw_icon_line as IconFn), (Tool::Rect, toolbar_icons::draw_icon_rect as IconFn), diff --git a/src/draw/render/shapes.rs b/src/draw/render/shapes.rs index acaf42a0..3f7dd349 100644 --- a/src/draw/render/shapes.rs +++ b/src/draw/render/shapes.rs @@ -4,12 +4,12 @@ use super::strokes::{ render_freehand_borrowed, render_freehand_pressure_borrowed, render_marker_stroke_borrowed, }; use super::text::{render_sticky_note, render_text}; +use crate::draw::Color; use crate::draw::shape::Shape; use crate::draw::shape::{ ARROW_LABEL_BACKGROUND, arrow_label_layout, measure_text_with_context, step_marker_outline_thickness, step_marker_radius, }; -use crate::draw::Color; /// Renders a single shape to a Cairo context. /// diff --git a/src/draw/shape/mod.rs b/src/draw/shape/mod.rs index 49349817..5c3780fc 100644 --- a/src/draw/shape/mod.rs +++ b/src/draw/shape/mod.rs @@ -15,7 +15,9 @@ pub(crate) use bounds::{ bounding_box_for_arrow, bounding_box_for_ellipse, bounding_box_for_eraser, bounding_box_for_line, bounding_box_for_points, bounding_box_for_rect, }; -pub(crate) use step_marker::{step_marker_bounds, step_marker_outline_thickness, step_marker_radius}; +pub(crate) use step_marker::{ + step_marker_bounds, step_marker_outline_thickness, step_marker_radius, +}; pub(crate) use text::{ bounding_box_for_sticky_note, bounding_box_for_text, sticky_note_layout, sticky_note_text_layout, diff --git a/src/draw/shape/step_marker.rs b/src/draw/shape/step_marker.rs index 6d5be92b..9e090cb6 100644 --- a/src/draw/shape/step_marker.rs +++ b/src/draw/shape/step_marker.rs @@ -7,11 +7,7 @@ const STEP_MARKER_PADDING_RATIO: f64 = 0.45; const STEP_MARKER_PADDING_MIN: f64 = 6.0; const STEP_MARKER_MIN_RADIUS: f64 = 10.0; -pub(crate) fn step_marker_radius( - value: u32, - size: f64, - font_descriptor: &FontDescriptor, -) -> f64 { +pub(crate) fn step_marker_radius(value: u32, size: f64, font_descriptor: &FontDescriptor) -> f64 { let text = value.to_string(); let font_desc_str = font_descriptor.to_pango_string(size); let max_dim = measure_text_cached(&text, &font_desc_str, size, None) diff --git a/src/draw/shape/tests.rs b/src/draw/shape/tests.rs index 0b6292e8..03f4aef2 100644 --- a/src/draw/shape/tests.rs +++ b/src/draw/shape/tests.rs @@ -1,6 +1,6 @@ use super::EraserBrush; use super::types::Shape; -use crate::draw::{EraserKind, FontDescriptor, color::WHITE}; +use crate::draw::{EraserKind, FontDescriptor, StepMarkerLabel, color::WHITE}; use crate::util; #[test] @@ -147,6 +147,31 @@ fn sticky_note_bounding_box_is_non_zero() { assert!(rect.y <= 20); } +#[test] +fn step_marker_bounding_box_is_square_and_contains_center() { + let font = FontDescriptor::default(); + let shape = Shape::StepMarker { + x: 120, + y: 80, + color: WHITE, + label: StepMarkerLabel { + value: 7, + size: 18.0, + font_descriptor: font, + }, + }; + + let rect = shape + .bounding_box() + .expect("step marker should have bounds"); + assert!(rect.width > 0); + assert_eq!(rect.width, rect.height); + assert!( + rect.contains(120, 80), + "step marker bounds should include center point" + ); +} + #[test] fn marker_bounding_box_uses_inflated_thickness() { let shape = Shape::MarkerStroke { diff --git a/src/input/hit_test/mod.rs b/src/input/hit_test/mod.rs index 2ee814e8..71a0ff89 100644 --- a/src/input/hit_test/mod.rs +++ b/src/input/hit_test/mod.rs @@ -6,9 +6,7 @@ mod shapes; #[cfg(test)] mod tests; -use crate::draw::shape::{ - arrow_label_layout, step_marker_outline_thickness, step_marker_radius, -}; +use crate::draw::shape::{arrow_label_layout, step_marker_outline_thickness, step_marker_radius}; use crate::draw::{DrawnShape, Shape}; use crate::util::Rect; diff --git a/src/input/hit_test/shapes.rs b/src/input/hit_test/shapes.rs index 104e2fe4..553616c2 100644 --- a/src/input/hit_test/shapes.rs +++ b/src/input/hit_test/shapes.rs @@ -119,13 +119,7 @@ pub(super) fn ellipse_outline_hit( outer && !inner } -pub(super) fn circle_hit( - cx: i32, - cy: i32, - radius: f64, - point: (i32, i32), - tolerance: f64, -) -> bool { +pub(super) fn circle_hit(cx: i32, cy: i32, radius: f64, point: (i32, i32), tolerance: f64) -> bool { let dx = point.0 as f64 - cx as f64; let dy = point.1 as f64 - cy as f64; let r = radius + tolerance.max(0.5); diff --git a/src/input/hit_test/tests.rs b/src/input/hit_test/tests.rs index da841c03..0473802d 100644 --- a/src/input/hit_test/tests.rs +++ b/src/input/hit_test/tests.rs @@ -1,5 +1,7 @@ use super::*; -use crate::draw::{ArrowLabel, BLACK, DrawnShape, EraserBrush, EraserKind, FontDescriptor, Shape}; +use crate::draw::{ + ArrowLabel, BLACK, DrawnShape, EraserBrush, EraserKind, FontDescriptor, Shape, StepMarkerLabel, +}; #[test] fn compute_hit_bounds_inflates_bounds_for_tolerance() { @@ -155,6 +157,44 @@ fn arrow_label_hit_detects_label_bounds() { ); } +#[test] +fn step_marker_hit_detects_center_and_rejects_outside_point() { + let font = FontDescriptor::default(); + let label = StepMarkerLabel { + value: 7, + size: 16.0, + font_descriptor: font, + }; + let drawn = DrawnShape { + id: 4, + shape: Shape::StepMarker { + x: 50, + y: 60, + color: BLACK, + label, + }, + created_at: 0, + locked: false, + }; + + assert!( + hit_test(&drawn, (50, 60), 0.1), + "center point should hit step marker" + ); + + let Shape::StepMarker { label, .. } = &drawn.shape else { + panic!("expected step marker shape"); + }; + let radius = + crate::draw::shape::step_marker_radius(label.value, label.size, &label.font_descriptor); + let outline = crate::draw::shape::step_marker_outline_thickness(label.size); + let outside_x = 50 + (radius + outline / 2.0).ceil() as i32 + 2; + assert!( + !hit_test(&drawn, (outside_x, 60), 0.1), + "point outside radius should miss step marker" + ); +} + #[test] fn distance_point_to_segment_matches_point_distance_for_zero_length_segment() { let start = (10, 10); diff --git a/src/input/state/core/utility/mod.rs b/src/input/state/core/utility/mod.rs index e3e13404..fb692911 100644 --- a/src/input/state/core/utility/mod.rs +++ b/src/input/state/core/utility/mod.rs @@ -1,6 +1,5 @@ mod actions; mod arrow_labels; -mod step_markers; mod font; mod frozen_zoom; mod help_overlay; @@ -8,4 +7,5 @@ mod interaction; mod launcher; mod pending; mod presenter_mode; +mod step_markers; mod toasts; diff --git a/src/input/state/tests/mod.rs b/src/input/state/tests/mod.rs index 724a2335..c359cf93 100644 --- a/src/input/state/tests/mod.rs +++ b/src/input/state/tests/mod.rs @@ -13,11 +13,11 @@ mod basics; mod board_picker; mod drawing; mod erase; -mod step_markers; mod menus; mod presenter_mode; mod pressure_modes; mod selection; +mod step_markers; mod text_edit; mod text_input; mod transform; diff --git a/src/input/state/tests/step_markers.rs b/src/input/state/tests/step_markers.rs index 74b6a633..cfc55100 100644 --- a/src/input/state/tests/step_markers.rs +++ b/src/input/state/tests/step_markers.rs @@ -2,6 +2,7 @@ use super::*; use crate::draw::StepMarkerLabel; use crate::input::{BOARD_ID_BLACKBOARD, BOARD_ID_WHITEBOARD}; +use crate::ui::toolbar::ToolbarEvent; fn step_marker_with_label(value: u32, font_descriptor: &FontDescriptor) -> Shape { Shape::StepMarker { @@ -46,3 +47,54 @@ fn sync_step_marker_counter_uses_max_across_boards() { state.sync_step_marker_counter(); assert_eq!(state.step_marker_counter, 10); } + +#[test] +fn next_step_marker_label_clamps_size() { + let mut state = create_test_input_state(); + + state.current_font_size = 10.0; + let label = state.next_step_marker_label(); + assert_eq!(label.size, 12.0); + + state.current_font_size = 100.0; + let label = state.next_step_marker_label(); + assert_eq!(label.size, 36.0); +} + +#[test] +fn toolbar_reset_step_marker_counter_resets_to_one() { + let mut state = create_test_input_state(); + state.step_marker_counter = 5; + + let changed = state.apply_toolbar_event(ToolbarEvent::ResetStepMarkerCounter); + + assert!(changed); + assert_eq!(state.step_marker_counter, 1); +} + +#[test] +fn drawing_step_marker_increments_counter() { + let mut state = create_test_input_state(); + state.set_tool_override(Some(Tool::StepMarker)); + + state.on_mouse_press(MouseButton::Left, 10, 10); + state.on_mouse_release(MouseButton::Left, 10, 10); + state.on_mouse_press(MouseButton::Left, 20, 20); + state.on_mouse_release(MouseButton::Left, 20, 20); + + let shapes = &state.boards.active_frame().shapes; + assert_eq!(shapes.len(), 2); + + let first_value = match &shapes[0].shape { + Shape::StepMarker { label, .. } => label.value, + _ => panic!("expected step marker for first shape"), + }; + let second_value = match &shapes[1].shape { + Shape::StepMarker { label, .. } => label.value, + _ => panic!("expected step marker for second shape"), + }; + + assert_eq!(first_value, 1); + assert_eq!(second_value, 2); + assert_eq!(state.step_marker_counter, 3); +} diff --git a/src/ui/toolbar/apply/mod.rs b/src/ui/toolbar/apply/mod.rs index dae0d0af..1f72cecd 100644 --- a/src/ui/toolbar/apply/mod.rs +++ b/src/ui/toolbar/apply/mod.rs @@ -27,9 +27,7 @@ impl InputState { self.apply_toolbar_toggle_arrow_labels(enable) } ToolbarEvent::ResetArrowLabelCounter => self.apply_toolbar_reset_arrow_label_counter(), - ToolbarEvent::ResetStepMarkerCounter => { - self.apply_toolbar_reset_step_marker_counter() - } + ToolbarEvent::ResetStepMarkerCounter => self.apply_toolbar_reset_step_marker_counter(), ToolbarEvent::SetUndoDelay(delay_secs) => self.apply_toolbar_set_undo_delay(delay_secs), ToolbarEvent::SetRedoDelay(delay_secs) => self.apply_toolbar_set_redo_delay(delay_secs), ToolbarEvent::SetCustomUndoDelay(delay_secs) => {