diff --git a/config.example.toml b/config.example.toml index d2092f76..2c30b50d 100644 --- a/config.example.toml +++ b/config.example.toml @@ -427,6 +427,46 @@ hidden = [ "top.utility.screenshot", ] +[ui.toolbar.items.order] +# Optional order overrides. Empty lists use the built-in order. +# Known IDs omitted from a non-empty list are appended in the default order. +# Side section ordering uses runtime block representatives; detailed sections +# such as eraser-mode, polygon-sides, and font remain visibility-only IDs. +top_tools = [ + "top.tool.select", + "top.tool.pen", + "top.tool.marker", + "top.tool.step-marker", + "top.tool.eraser", + "top.tool.line", + "top.tool.rect", + "top.tool.ellipse", + "top.tool.arrow", + "top.tool.blur", +] +top_controls = [ + "top.utility.text", + "top.utility.sticky-note", + "top.utility.screenshot", + "top.utility.clear-canvas", + "top.utility.highlight", +] +side_sections = [ + "side.group.colors", + "side.group.presets", + "side.group.thickness", + "side.group.arrow-labels", + "side.group.step-markers", + "side.group.marker-opacity", + "side.group.text-size", + "side.group.actions", + "side.group.boards", + "side.group.pages", + "side.group.step-undo", + "side.group.session", + "side.group.settings", +] + # ─────────────────────────────────────────────────────────────────────────────── # Status Bar Styling # ─────────────────────────────────────────────────────────────────────────────── diff --git a/configurator/src/app/search/tests.rs b/configurator/src/app/search/tests.rs index 81c8c157..4912e4a8 100644 --- a/configurator/src/app/search/tests.rs +++ b/configurator/src/app/search/tests.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use crate::models::session::SessionArtifactSummary; use crate::models::{KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId}; +use wayscriber::config::toolbar_item_ids as ids; #[test] fn active_search_tab_click_corrects_keybindings_nested_tab() { @@ -289,8 +290,8 @@ fn parent_scoped_ui_queries_match_concrete_nested_tabs() { fn ui_nested_visible_control_labels_match_concrete_nested_tabs() { let cases = [ ("layout mode", UiTabId::Toolbar), - ("top.tool.blur", UiTabId::ToolbarVisibility), - ("side.group.presets", UiTabId::ToolbarVisibility), + (ids::TOP_TOOL_BLUR.as_str(), UiTabId::ToolbarVisibility), + (ids::SIDE_GROUP_PRESETS.as_str(), UiTabId::ToolbarVisibility), ("status bar position", UiTabId::StatusBar), ("click highlight radius", UiTabId::ClickHighlight), ]; diff --git a/configurator/src/app/update/fields.rs b/configurator/src/app/update/fields.rs index dc001192..39a51276 100644 --- a/configurator/src/app/update/fields.rs +++ b/configurator/src/app/update/fields.rs @@ -1,4 +1,5 @@ use iced::Task; +use wayscriber::config::{ToolbarItemId, ToolbarItemOrderGroup}; use crate::messages::Message; use crate::models::{ @@ -12,7 +13,6 @@ use crate::models::{ }; #[cfg(feature = "tablet-input")] use crate::models::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; -use wayscriber::config::ToolbarItemId; use super::super::state::{ConfiguratorApp, StatusMessage}; @@ -180,6 +180,28 @@ impl ConfiguratorApp { Task::none() } + pub(super) fn handle_toolbar_item_move_requested( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + delta: isize, + ) -> Task { + self.status = StatusMessage::idle(); + self.draft.move_toolbar_item(group, id, delta); + self.refresh_dirty_flag(); + Task::none() + } + + pub(super) fn handle_toolbar_item_order_reset( + &mut self, + group: ToolbarItemOrderGroup, + ) -> Task { + self.status = StatusMessage::idle(); + self.draft.reset_toolbar_item_order(group); + self.refresh_dirty_flag(); + Task::none() + } + pub(super) fn handle_session_storage_mode_changed( &mut self, option: SessionStorageModeOption, diff --git a/configurator/src/app/update/mod.rs b/configurator/src/app/update/mod.rs index 8be465fe..1bd1c736 100644 --- a/configurator/src/app/update/mod.rs +++ b/configurator/src/app/update/mod.rs @@ -122,6 +122,10 @@ impl ConfiguratorApp { Message::ToolbarItemVisibilityChanged(id, visible) => { self.handle_toolbar_item_visibility_changed(id, visible) } + Message::ToolbarItemMoveRequested(group, id, delta) => { + self.handle_toolbar_item_move_requested(group, id, delta) + } + Message::ToolbarItemOrderReset(group) => self.handle_toolbar_item_order_reset(group), Message::BoardsAddItem => self.handle_boards_add_item(), Message::BoardsRemoveItem(index) => self.handle_boards_remove_item(index), Message::BoardsMoveItemUp(index) => self.handle_boards_move_item(index, true), diff --git a/configurator/src/app/view/ui/toolbar.rs b/configurator/src/app/view/ui/toolbar.rs index 5b5c117d..9e5c815d 100644 --- a/configurator/src/app/view/ui/toolbar.rs +++ b/configurator/src/app/view/ui/toolbar.rs @@ -1,8 +1,10 @@ -use iced::widget::{checkbox, column, pick_list, row, scrollable, text}; +use std::collections::BTreeSet; + +use iced::widget::{button, checkbox, column, pick_list, row, scrollable, text}; use iced::{Element, Length}; use wayscriber::config::{ - ResolvedToolbarItems, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemSurface, - ToolbarItemsConfig, toolbar_item_definitions, + ResolvedToolbarItems, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemOrderGroup, + ToolbarItemSurface, ToolbarItemsConfig, toolbar_item_definitions, toolbar_item_order_group, }; use crate::app::scroll::CONTENT_SCROLL_ID; @@ -255,7 +257,7 @@ fn toolbar_item_visibility_section<'a>( ); } - for definition in toolbar_item_definitions() { + for definition in toolbar_item_definitions_for_display(&resolved) { if current_surface != Some(definition.surface) { current_surface = Some(definition.surface); current_category = None; @@ -287,17 +289,81 @@ fn toolbar_item_visibility_row<'a>( "default: {}", visibility_override_label(!defaults.is_hidden(id)) ); + let order_group = configurator_order_group(definition); + let order = order_group.and_then(|group| { + let index = resolved.order.index_of(group, id)?; + let len = resolved.order.ordered_ids(group).len(); + Some((group, index, index > 0, index + 1 < len)) + }); - row![ + let mut cells = row![ checkbox(visible) .label(definition.label) .on_toggle(move |value| Message::ToolbarItemVisibilityChanged(id, value)), text(definition.id.as_str()).size(12).width(Length::Fill), - text(default).size(12), ] .spacing(12) - .align_y(iced::Alignment::Center) - .into() + .align_y(iced::Alignment::Center); + if let Some((group, _, can_move_up, can_move_down)) = order { + let up = if can_move_up { + button(text("^")).on_press(Message::ToolbarItemMoveRequested(group, id, -1)) + } else { + button(text("^")) + }; + let down = if can_move_down { + button(text("v")).on_press(Message::ToolbarItemMoveRequested(group, id, 1)) + } else { + button(text("v")) + }; + cells = cells + .push(up) + .push(down) + .push(button(text("Reset")).on_press(Message::ToolbarItemOrderReset(group))); + } + cells.push(text(default).size(12)).into() +} + +fn toolbar_item_definitions_for_display( + resolved: &ResolvedToolbarItems, +) -> Vec<&'static ToolbarItemDefinition> { + let mut result = Vec::new(); + let mut emitted = BTreeSet::new(); + let mut emitted_groups = BTreeSet::new(); + + for definition in toolbar_item_definitions() { + if let Some(group) = configurator_order_group(definition) { + if emitted_groups.insert(group) { + for id in resolved.order.ordered_ids(group) { + if let Some(ordered_definition) = toolbar_item_definitions() + .iter() + .find(|candidate| candidate.id == *id) + && configurator_order_group(ordered_definition) == Some(group) + { + result.push(ordered_definition); + emitted.insert(ordered_definition.id); + } + } + } + if emitted.contains(&definition.id) { + continue; + } + } + result.push(definition); + emitted.insert(definition.id); + } + + result +} + +fn configurator_order_group(definition: &ToolbarItemDefinition) -> Option { + let group = toolbar_item_order_group(definition)?; + matches!( + group, + ToolbarItemOrderGroup::TopTools + | ToolbarItemOrderGroup::TopControls + | ToolbarItemOrderGroup::SideSections + ) + .then_some(group) } fn visibility_override_label(visible: bool) -> &'static str { diff --git a/configurator/src/messages.rs b/configurator/src/messages.rs index f21c6e4d..f03f16f6 100644 --- a/configurator/src/messages.rs +++ b/configurator/src/messages.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use std::sync::Arc; -use wayscriber::config::{Config, ToolbarItemId}; +use wayscriber::config::{Config, ToolbarItemId, ToolbarItemOrderGroup}; use crate::models::{ BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode, ColorPickerId, @@ -74,6 +74,8 @@ pub enum Message { ToolbarOverrideModeChanged(ToolbarLayoutModeOption), ToolbarOverrideChanged(ToolbarOverrideField, OverrideOption), ToolbarItemVisibilityChanged(ToolbarItemId, bool), + ToolbarItemMoveRequested(ToolbarItemOrderGroup, ToolbarItemId, isize), + ToolbarItemOrderReset(ToolbarItemOrderGroup), BoardsAddItem, BoardsRemoveItem(usize), BoardsMoveItemUp(usize), diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs index 5ebb6313..cf6b5b6a 100644 --- a/configurator/src/models/config/setters.rs +++ b/configurator/src/models/config/setters.rs @@ -4,7 +4,7 @@ use super::super::fields::{ ToolbarOverrideField, TripletField, }; use super::draft::ConfigDraft; -use wayscriber::config::ToolbarItemId; +use wayscriber::config::{ToolbarItemId, ToolbarItemOrderGroup}; impl ConfigDraft { pub fn apply_toolbar_layout_mode(&mut self, mode: ToolbarLayoutModeOption) { @@ -36,6 +36,19 @@ impl ConfigDraft { self.ui_toolbar_items.set_hidden(id, !visible); } + pub fn move_toolbar_item( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + delta: isize, + ) { + self.ui_toolbar_items.move_item_by(group, id, delta); + } + + pub fn reset_toolbar_item_order(&mut self, group: ToolbarItemOrderGroup) { + self.ui_toolbar_items.reset_known_order_to_defaults(group); + } + pub fn set_mouse_drag_tool( &mut self, button: DragMouseButton, diff --git a/configurator/src/models/config/tests.rs b/configurator/src/models/config/tests.rs index 27c640cf..925c2ff8 100644 --- a/configurator/src/models/config/tests.rs +++ b/configurator/src/models/config/tests.rs @@ -10,8 +10,8 @@ use super::{ConfigDraft, RenderProfileSelectionOption}; use wayscriber::config::{ ColorSpec, Config, PdfFitMode, PdfLabelContentMode, PdfLabelPosition, PdfOrientation, PdfPageSize, PdfTransparentBackground, PresetToolStatesConfig, RenderColorMappingConfig, - RenderProfileConfig, RenderProfileExportMode, ToolPresetConfig, ToolbarItemsConfig, - XdgFocusLossBehavior, + RenderProfileConfig, RenderProfileExportMode, ToolPresetConfig, ToolbarItemOrderConfig, + ToolbarItemOrderGroup, ToolbarItemsConfig, XdgFocusLossBehavior, toolbar_item_ids as ids, }; use wayscriber::input::{DragTool, PerToolDrawingSettings, Tool}; @@ -69,14 +69,15 @@ fn config_draft_round_trips_toolbar_item_visibility_preserving_unknown_ids() { config.ui.toolbar.items = ToolbarItemsConfig { hidden: vec![ "future.toolbar.item".to_string(), - "side.actions.undo-all".to_string(), - "side.actions.undo-all".to_string(), + ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(), + ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(), ], + order: ToolbarItemOrderConfig::default(), }; let mut draft = ConfigDraft::from_config(&config); - draft.set_toolbar_item_visible("side.actions.undo-all".parse().expect("known id"), true); - draft.set_toolbar_item_visible("top.tool.pen".parse().expect("known id"), false); + draft.set_toolbar_item_visible(ids::SIDE_ACTIONS_UNDO_ALL, true); + draft.set_toolbar_item_visible(ids::TOP_TOOL_PEN, false); let round_trip = draft .to_config(&config) @@ -84,7 +85,37 @@ fn config_draft_round_trips_toolbar_item_visibility_preserving_unknown_ids() { assert_eq!( round_trip.ui.toolbar.items.hidden, - vec!["future.toolbar.item", "top.tool.pen"] + vec![ + "future.toolbar.item".to_string(), + ids::TOP_TOOL_PEN.as_str().to_string() + ] + ); +} + +#[test] +fn config_draft_round_trips_toolbar_item_order_preserving_unknown_ids() { + let mut config = Config::default(); + config.ui.toolbar.items.order.top_tools = vec![ + "future.toolbar.item".to_string(), + ids::TOP_TOOL_PEN.as_str().to_string(), + ids::TOP_TOOL_SELECT.as_str().to_string(), + ]; + + let mut draft = ConfigDraft::from_config(&config); + draft.move_toolbar_item(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN, 1); + + let round_trip = draft + .to_config(&config) + .expect("expected config to round trip"); + + assert!( + round_trip + .ui + .toolbar + .items + .order + .top_tools + .contains(&"future.toolbar.item".to_string()) ); } diff --git a/docs/CONFIG.md b/docs/CONFIG.md index ae11fa64..152c8723 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -566,6 +566,35 @@ hidden = [ "side.actions.undo-all", "side.group.presets", ] + +[ui.toolbar.items.order] +# Optional order overrides. Empty lists use the built-in order. +# Known IDs omitted from a non-empty list append in the default order. +# Side section ordering uses runtime block representatives; detailed sections +# such as eraser-mode, polygon-sides, and font remain visibility-only IDs. +top_tools = [ + "top.tool.select", + "top.tool.pen", + "top.tool.marker", + "top.tool.step-marker", + "top.tool.eraser", +] +top_controls = [ + "top.utility.text", + "top.utility.sticky-note", + "top.utility.screenshot", + "top.utility.clear-canvas", + "top.utility.highlight", +] +side_sections = [ + "side.group.colors", + "side.group.presets", + "side.group.thickness", + "side.group.actions", + "side.group.pages", + "side.group.boards", + "side.group.settings", +] ``` **Behavior:** @@ -591,6 +620,8 @@ hidden = [ - **Force inline**: `force_inline` (or `WAYSCRIBER_FORCE_INLINE_TOOLBARS`) skips layer-shell toolbars. - **Pinned**: `top_pinned`/`side_pinned` control whether each toolbar opens on startup. - **Hidden items**: `ui.toolbar.items.hidden` removes known toolbar buttons/sections from sizing, drawing, and hit testing while preserving unknown future IDs. +- **Item order**: `ui.toolbar.items.order.top_tools`, `top_controls`, and `side_sections` reorder supported toolbar items. `side_sections` orders runtime block representatives; `side.group.eraser-mode`, `side.group.polygon-sides`, and `side.group.font` can be hidden individually but are not independently orderable. Unknown future IDs and wrong-group IDs are ignored at runtime but preserved across saves. +- **Live customization**: the overlay Customize tab supports show/hide, move up/down, and drag reorder for supported groups. The configurator supports the same saved order with up/down controls. - **Screenshot toolbar button**: `top.utility.screenshot` is hidden by default; remove it from `ui.toolbar.items.hidden` or enable it in the configurator/overlay customization to show it. **Defaults:** all set as above. diff --git a/src/backend/wayland/state/toolbar/visibility/access.rs b/src/backend/wayland/state/toolbar/visibility/access.rs index 5ed9130b..74245c04 100644 --- a/src/backend/wayland/state/toolbar/visibility/access.rs +++ b/src/backend/wayland/state/toolbar/visibility/access.rs @@ -17,6 +17,9 @@ impl WaylandState { pub(in crate::backend::wayland) fn set_toolbar_dragging(&mut self, value: bool) { self.data.toolbar_dragging = value; + if !value { + self.input_state.clear_toolbar_item_drag(); + } } pub(in crate::backend::wayland) fn toolbar_drag_preview_active(&self) -> bool { diff --git a/src/backend/wayland/toolbar/events.rs b/src/backend/wayland/toolbar/events.rs index 060e2a93..32b895fd 100644 --- a/src/backend/wayland/toolbar/events.rs +++ b/src/backend/wayland/toolbar/events.rs @@ -1,19 +1,36 @@ +use crate::config::{ToolbarItemId, ToolbarItemOrderGroup}; use crate::draw::Color; /// Kinds of hit regions and their drag semantics. #[derive(Clone, Debug, PartialEq)] pub enum HitKind { Click, - DragSetThickness { min: f64, max: f64 }, - DragSetMarkerOpacity { min: f64, max: f64 }, + DragSetThickness { + min: f64, + max: f64, + }, + DragSetMarkerOpacity { + min: f64, + max: f64, + }, DragSetFontSize, - PickColor { x: f64, y: f64, w: f64, h: f64 }, + PickColor { + x: f64, + y: f64, + w: f64, + h: f64, + }, DragUndoDelay, DragRedoDelay, DragCustomUndoDelay, DragCustomRedoDelay, DragMoveTop, DragMoveSide, + DragToolbarItem { + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + target_index: usize, + }, } /// Cursor hint for toolbar regions. @@ -42,7 +59,8 @@ impl HitKind { | HitKind::DragCustomUndoDelay | HitKind::DragCustomRedoDelay | HitKind::DragMoveTop - | HitKind::DragMoveSide => ToolbarCursorHint::Grab, + | HitKind::DragMoveSide + | HitKind::DragToolbarItem { .. } => ToolbarCursorHint::Grab, HitKind::PickColor { .. } => ToolbarCursorHint::Crosshair, } } diff --git a/src/backend/wayland/toolbar/hit.rs b/src/backend/wayland/toolbar/hit.rs index 4479d422..39169115 100644 --- a/src/backend/wayland/toolbar/hit.rs +++ b/src/backend/wayland/toolbar/hit.rs @@ -38,6 +38,7 @@ pub fn intent_for_hit(hit: &HitRegion, x: f64, y: f64) -> Option<(ToolbarIntent, | HitKind::DragCustomRedoDelay | HitKind::DragMoveTop | HitKind::DragMoveSide + | HitKind::DragToolbarItem { .. } ); use crate::backend::wayland::toolbar::events::HitKind::*; @@ -107,6 +108,7 @@ pub fn intent_for_hit(hit: &HitRegion, x: f64, y: f64) -> Option<(ToolbarIntent, ), DragMoveTop => ToolbarEvent::MoveTopToolbar { x, y }, DragMoveSide => ToolbarEvent::MoveSideToolbar { x, y }, + DragToolbarItem { group, id, .. } => ToolbarEvent::StartToolbarItemDrag { group, id }, crate::backend::wayland::toolbar::events::HitKind::Click => hit.event.clone(), }; @@ -185,6 +187,14 @@ pub fn drag_intent_for_hit(hit: &HitRegion, x: f64, y: f64) -> Option Some(ToolbarIntent(ToolbarEvent::MoveTopToolbar { x, y })), DragMoveSide => Some(ToolbarIntent(ToolbarEvent::MoveSideToolbar { x, y })), + DragToolbarItem { + group, + target_index, + .. + } => Some(ToolbarIntent(ToolbarEvent::DragToolbarItemOver { + group, + target_index, + })), _ => None, } } diff --git a/src/backend/wayland/toolbar/layout/side/mod.rs b/src/backend/wayland/toolbar/layout/side/mod.rs index ac1177f5..97eb75da 100644 --- a/src/backend/wayland/toolbar/layout/side/mod.rs +++ b/src/backend/wayland/toolbar/layout/side/mod.rs @@ -49,71 +49,106 @@ pub fn build_side_hits( return; } - // Color section: only when tool needs color - if tool_context.needs_color && !snapshot.side_section_hidden(ToolbarSideSection::Colors) { - y = colors::push_color_picker_hits(&ctx, y, hits); - } - - if !snapshot.side_section_hidden(ToolbarSideSection::Presets) { - y = presets::push_preset_hits(&ctx, y, hits); - } - - // Thickness/size: only when tool needs it - if tool_context.needs_thickness { - y = sliders::push_thickness_hits(&ctx, y, hits); - - if tool_context.show_eraser_mode - && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) - { - y = sliders::push_eraser_mode_hits(&ctx, y, hits); + let mut drawer_tabs_pushed = false; + let mut thickness_block_pushed = false; + let mut text_block_pushed = false; + for section in crate::ui::toolbar::model::ordered_side_sections(snapshot) { + match section { + ToolbarSideSection::Colors + if tool_context.needs_color + && !snapshot.side_section_hidden(ToolbarSideSection::Colors) => + { + y = colors::push_color_picker_hits(&ctx, y, hits); + } + ToolbarSideSection::Presets + if !snapshot.side_section_hidden(ToolbarSideSection::Presets) => + { + y = presets::push_preset_hits(&ctx, y, hits); + } + ToolbarSideSection::Thickness + | ToolbarSideSection::EraserMode + | ToolbarSideSection::PolygonSides + if tool_context.needs_thickness && !thickness_block_pushed => + { + thickness_block_pushed = true; + y = sliders::push_thickness_hits(&ctx, y, hits); + if tool_context.show_eraser_mode + && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) + { + y = sliders::push_eraser_mode_hits(&ctx, y, hits); + } + if tool_context.show_polygon_sides_control + && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) + { + y = sliders::push_polygon_sides_hits(&ctx, y, hits); + } + } + ToolbarSideSection::ArrowLabels + if tool_context.show_arrow_labels + && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) => + { + y = arrow::push_arrow_section_hits(&ctx, y, hits); + } + ToolbarSideSection::StepMarkers + if tool_context.show_step_counter + && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) => + { + y = arrow::push_step_marker_hits(&ctx, y, hits); + } + ToolbarSideSection::MarkerOpacity + if tool_context.show_marker_opacity + && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) => + { + y = sliders::push_marker_opacity_hits(&ctx, y, hits); + } + ToolbarSideSection::TextSize | ToolbarSideSection::Font + if tool_context.show_font_controls && !text_block_pushed => + { + text_block_pushed = true; + y = sliders::push_text_size_hits(&ctx, y, hits); + y = sliders::push_font_hits(&ctx, y, hits); + } + ToolbarSideSection::Actions => { + y = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); + y = actions::push_actions_hits(&ctx, y, hits); + } + ToolbarSideSection::Boards => { + y = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); + y = boards::push_boards_hits(&ctx, y, hits); + } + ToolbarSideSection::Pages => { + y = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); + y = pages::push_pages_hits(&ctx, y, hits); + } + ToolbarSideSection::StepUndo => { + y = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); + y = delay::push_delay_hits(&ctx, y, hits); + } + ToolbarSideSection::Session => { + y = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); + y = session::push_session_hits(&ctx, y, hits); + } + ToolbarSideSection::Settings => { + y = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); + settings::push_settings_hits(&ctx, y, hits); + } + _ => {} } - - if tool_context.show_polygon_sides_control - && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) - { - y = sliders::push_polygon_sides_hits(&ctx, y, hits); - } - } - - // Arrow section: only for arrow tool - if tool_context.show_arrow_labels - && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) - { - y = arrow::push_arrow_section_hits(&ctx, y, hits); - } - - // Step marker counter: only for step marker tool - if tool_context.show_step_counter - && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) - { - y = arrow::push_step_marker_hits(&ctx, y, hits); - } - - // Marker opacity: only for marker tool - if tool_context.show_marker_opacity - && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) - { - y = sliders::push_marker_opacity_hits(&ctx, y, hits); } + let _ = push_drawer_tabs_hits_once(&ctx, y, hits, &mut drawer_tabs_pushed); +} - // Text controls: only when text/note is active - if tool_context.show_font_controls { - if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { - y = sliders::push_text_size_hits(&ctx, y, hits); - } - if !snapshot.side_section_hidden(ToolbarSideSection::Font) { - y = sliders::push_font_hits(&ctx, y, hits); - } +fn push_drawer_tabs_hits_once( + ctx: &SideLayoutContext<'_>, + y: f64, + hits: &mut Vec, + pushed: &mut bool, +) -> f64 { + if *pushed { + return y; } - - y = drawer::push_drawer_tabs_hits(&ctx, y, hits); - y = actions::push_actions_hits(&ctx, y, hits); - y = boards::push_boards_hits(&ctx, y, hits); - y = pages::push_pages_hits(&ctx, y, hits); - y = delay::push_delay_hits(&ctx, y, hits); - y = session::push_session_hits(&ctx, y, hits); - - settings::push_settings_hits(&ctx, y, hits); + *pushed = true; + drawer::push_drawer_tabs_hits(ctx, y, hits) } pub(super) struct SideLayoutContext<'a> { diff --git a/src/backend/wayland/toolbar/layout/side/settings.rs b/src/backend/wayland/toolbar/layout/side/settings.rs index bee53212..53b49829 100644 --- a/src/backend/wayland/toolbar/layout/side/settings.rs +++ b/src/backend/wayland/toolbar/layout/side/settings.rs @@ -120,12 +120,54 @@ pub(super) fn push_settings_hits(ctx: &SideLayoutContext<'_>, y: f64, hits: &mut item_overrides.len(), ); for (item, override_item) in item_layout.items.iter().zip(item_overrides.iter()) { + let order = override_item.order.as_ref(); + let order_gap = 4.0; + let handle_w = if order.is_some() { 24.0 } else { 0.0 }; + let move_btn_w = if order.is_some() { 28.0 } else { 0.0 }; + let move_buttons_w = if order.is_some() { + move_btn_w * 2.0 + order_gap * 2.0 + } else { + 0.0 + }; + let checkbox_x = item.x + handle_w + if order.is_some() { order_gap } else { 0.0 }; + let checkbox_w = + item.w - handle_w - move_buttons_w - if order.is_some() { order_gap } else { 0.0 }; hits.push(HitRegion { - rect: (item.x, item.y, item.w, item.h), + rect: (checkbox_x, item.y, checkbox_w, item.h), event: activation_event(&override_item.activation), kind: HitKind::Click, tooltip: override_item.tooltip.as_string(), }); + if let Some(order) = order { + let up_x = item.x + item.w - move_btn_w * 2.0 - order_gap; + let down_x = item.x + item.w - move_btn_w; + for (button_x, enabled, activation, tooltip) in [ + (up_x, order.can_move_up, &order.move_up, "Move up"), + (down_x, order.can_move_down, &order.move_down, "Move down"), + ] { + if enabled { + hits.push(HitRegion { + rect: (button_x, item.y, move_btn_w, item.h), + event: activation_event(activation), + kind: HitKind::Click, + tooltip: Some(format!("{} {}", tooltip, override_item.label)), + }); + } + } + hits.push(HitRegion { + rect: (item.x, item.y, item.w, item.h), + event: crate::ui::toolbar::ToolbarEvent::StartToolbarItemDrag { + group: order.group, + id: override_item.id, + }, + kind: HitKind::DragToolbarItem { + group: order.group, + id: override_item.id, + target_index: order.index, + }, + tooltip: Some(format!("Drag {} to reorder", override_item.label)), + }); + } } } diff --git a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs index a79d0236..d7955978 100644 --- a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs +++ b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs @@ -1,7 +1,7 @@ use crate::backend::wayland::toolbar::rows::capped_grid_columns; use crate::ui::toolbar::model::{ ToolbarActionsModel, ToolbarCommandGroupKind, ToolbarSessionModel, ToolbarSettingsModel, - toolbar_boards_model, toolbar_pages_model, + ordered_side_sections, toolbar_boards_model, toolbar_pages_model, }; use crate::ui::toolbar::snapshot::ToolContext; use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; @@ -62,92 +62,109 @@ impl ToolbarLayoutSpec { return (Self::SIDE_WIDTH, height.ceil() as u32); } - if tool_context.needs_color && !snapshot.side_section_hidden(ToolbarSideSection::Colors) { - let colors_h = self.side_colors_height(snapshot); - add_section(colors_h, &mut height); - } - - if show_presets { - add_section(self.side_presets_height(snapshot), &mut height); - } - - if tool_context.needs_thickness { - if !snapshot.side_section_hidden(ToolbarSideSection::Thickness) { - add_section(self.side_thickness_height(snapshot), &mut height); - } - if tool_context.show_eraser_mode - && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) - { - add_section(self.side_eraser_mode_height(snapshot), &mut height); - } - if tool_context.show_polygon_sides_control - && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) - { - add_section(self.side_polygon_sides_height(snapshot), &mut height); - } - } - - if tool_context.show_arrow_labels - && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) - { - add_section(self.side_arrow_labels_height(snapshot), &mut height); - } - - if tool_context.show_step_counter - && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) - { - add_section(self.side_step_markers_height(snapshot), &mut height); - } - - if tool_context.show_marker_opacity - && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) - { - add_section(self.side_marker_opacity_height(snapshot), &mut height); - } - - if tool_context.show_font_controls { - if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { - add_section(self.side_text_size_height(snapshot), &mut height); - } - if !snapshot.side_section_hidden(ToolbarSideSection::Font) { - add_section(self.side_font_height(snapshot), &mut height); + let mut drawer_tabs_added = false; + let mut thickness_block_added = false; + let mut text_block_added = false; + for section in ordered_side_sections(snapshot) { + match section { + ToolbarSideSection::Colors + if tool_context.needs_color + && !snapshot.side_section_hidden(ToolbarSideSection::Colors) => + { + add_section(self.side_colors_height(snapshot), &mut height); + } + ToolbarSideSection::Presets if show_presets => { + add_section(self.side_presets_height(snapshot), &mut height); + } + ToolbarSideSection::Thickness + | ToolbarSideSection::EraserMode + | ToolbarSideSection::PolygonSides + if tool_context.needs_thickness && !thickness_block_added => + { + thickness_block_added = true; + if !snapshot.side_section_hidden(ToolbarSideSection::Thickness) { + add_section(self.side_thickness_height(snapshot), &mut height); + } + if tool_context.show_eraser_mode + && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) + { + add_section(self.side_eraser_mode_height(snapshot), &mut height); + } + if tool_context.show_polygon_sides_control + && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) + { + add_section(self.side_polygon_sides_height(snapshot), &mut height); + } + } + ToolbarSideSection::ArrowLabels + if tool_context.show_arrow_labels + && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) => + { + add_section(self.side_arrow_labels_height(snapshot), &mut height); + } + ToolbarSideSection::StepMarkers + if tool_context.show_step_counter + && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) => + { + add_section(self.side_step_markers_height(snapshot), &mut height); + } + ToolbarSideSection::MarkerOpacity + if tool_context.show_marker_opacity + && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) => + { + add_section(self.side_marker_opacity_height(snapshot), &mut height); + } + ToolbarSideSection::TextSize | ToolbarSideSection::Font + if tool_context.show_font_controls && !text_block_added => + { + text_block_added = true; + if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { + add_section(self.side_text_size_height(snapshot), &mut height); + } + if !snapshot.side_section_hidden(ToolbarSideSection::Font) { + add_section(self.side_font_height(snapshot), &mut height); + } + } + ToolbarSideSection::Actions => { + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); + if show_actions { + add_section(self.side_actions_height(snapshot), &mut height); + } + } + ToolbarSideSection::Boards => { + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); + if show_boards { + add_section(self.side_boards_height(snapshot), &mut height); + } + } + ToolbarSideSection::Pages => { + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); + if show_pages { + add_section(self.side_pages_height(snapshot), &mut height); + } + } + ToolbarSideSection::StepUndo => { + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); + if show_step_section { + add_section(self.side_step_height(snapshot), &mut height); + } + } + ToolbarSideSection::Session => { + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); + if show_session_section { + add_section(self.side_session_height(snapshot), &mut height); + } + } + ToolbarSideSection::Settings => { + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); + if show_settings_section { + add_section(self.side_settings_height(snapshot), &mut height); + } + } + _ => {} } } - - if snapshot.drawer_open { - let tabs_h = self.side_drawer_tabs_height(snapshot); - add_section(tabs_h, &mut height); - } - - if show_actions { - let actions_card_h = self.side_actions_height(snapshot); - add_section(actions_card_h, &mut height); - } - - if show_boards { - let boards_h = self.side_boards_height(snapshot); - add_section(boards_h, &mut height); - } - - if show_pages { - let pages_h = self.side_pages_height(snapshot); - add_section(pages_h, &mut height); - } - - if show_step_section { - let step_h = self.side_step_height(snapshot); - add_section(step_h, &mut height); - } - - if show_session_section { - let session_h = self.side_session_height(snapshot); - add_section(session_h, &mut height); - } - - if show_settings_section { - let settings_h = self.side_settings_height(snapshot); - add_section(settings_h, &mut height); - } + add_drawer_tabs_once(self, snapshot, &mut height, &mut drawer_tabs_added); height += Self::SIDE_FOOTER_PADDING; @@ -583,3 +600,19 @@ impl ToolbarLayoutSpec { } } } + +fn add_drawer_tabs_once( + spec: &ToolbarLayoutSpec, + snapshot: &ToolbarSnapshot, + height: &mut f64, + added: &mut bool, +) { + if *added || !snapshot.drawer_open { + return; + } + let tabs_h = spec.side_drawer_tabs_height(snapshot); + if tabs_h > 0.0 { + *height += tabs_h + ToolbarLayoutSpec::SIDE_SECTION_GAP; + } + *added = true; +} diff --git a/src/backend/wayland/toolbar/layout/spec/top.rs b/src/backend/wayland/toolbar/layout/spec/top.rs index 219c5ad4..46585efe 100644 --- a/src/backend/wayland/toolbar/layout/spec/top.rs +++ b/src/backend/wayland/toolbar/layout/spec/top.rs @@ -1,4 +1,4 @@ -use crate::config::ToolbarLayoutMode; +use crate::config::{ToolbarLayoutMode, toolbar_item_ids as ids}; use crate::ui::toolbar::ToolbarSnapshot; use crate::ui::toolbar::model; @@ -43,20 +43,10 @@ impl ToolbarLayoutSpec { } else { (Self::TOP_TEXT_BUTTON_W, Self::TOP_TEXT_BUTTON_H) }; - let row_count = if self.layout_mode == ToolbarLayoutMode::Simple { - let mut rows = 0.0; - if model::visible_tool_count(model::common_shape_tools(), snapshot) > 0 { - rows += 1.0; - } - if model::visible_tool_count(model::polygon_tools(), snapshot) > 0 { - rows += 1.0; - } - rows - } else if model::visible_tool_count(model::polygon_tools(), snapshot) > 0 { - 1.0 - } else { - 0.0 - }; + let row_count = model::visible_shape_picker_row_count( + snapshot, + self.layout_mode == ToolbarLayoutMode::Simple, + ) as f64; height += row_count * (btn_h + Self::TOP_SHAPE_ROW_GAP); } @@ -72,7 +62,7 @@ impl ToolbarLayoutSpec { ) .count(); let mut x = Self::TOP_START_X; - if model::toolbar_item_visible(snapshot, "top.chrome.drag") { + if model::toolbar_item_visible(snapshot, ids::TOP_CHROME_DRAG) { x += Self::TOP_HANDLE_SIZE + gap; } x += tool_count as f64 * (btn_w + gap); @@ -88,31 +78,40 @@ impl ToolbarLayoutSpec { if fill_visible { x += Self::TOP_TEXT_FILL_W + gap; } - if model::top_text_visible(snapshot) { - x += btn_w + gap; // Text button - } - if model::top_sticky_note_visible(snapshot) { - x += btn_w + gap; // Note button - } - if model::top_screenshot_visible(snapshot) { - x += btn_w + gap; // Screenshot - } - if self.layout_mode != ToolbarLayoutMode::Simple { - if model::top_clear_canvas_visible(snapshot) { - x += btn_w + gap; // Clear - } - if self.use_icons && model::top_highlight_visible(snapshot) { - x += btn_w + gap; // Highlight - } - } + x += model::visible_top_utility_buttons( + snapshot, + self.layout_mode == ToolbarLayoutMode::Simple, + self.use_icons, + ) + .len() as f64 + * (btn_w + gap); let left_end = if model::top_icon_mode_toggle_visible(snapshot) { x + Self::TOP_TOGGLE_WIDTH } else { x }; + let picker_right = if self.shape_picker_open && model::top_shape_picker_visible(snapshot) { + let picker_count = model::visible_shape_picker_max_row_len( + snapshot, + self.layout_mode == ToolbarLayoutMode::Simple, + ); + if picker_count == 0 { + 0.0 + } else { + let picker_x = Self::TOP_START_X + + if model::toolbar_item_visible(snapshot, ids::TOP_CHROME_DRAG) { + Self::TOP_HANDLE_SIZE + gap + } else { + 0.0 + }; + picker_x + picker_count as f64 * (btn_w + gap) + } + } else { + 0.0 + }; let right_control_count = - usize::from(model::toolbar_item_visible(snapshot, "top.chrome.pin")) - + usize::from(model::toolbar_item_visible(snapshot, "top.chrome.close")); + usize::from(model::toolbar_item_visible(snapshot, ids::TOP_CHROME_PIN)) + + usize::from(model::toolbar_item_visible(snapshot, ids::TOP_CHROME_CLOSE)); let right_controls = if right_control_count == 0 { 0.0 } else { @@ -120,7 +119,7 @@ impl ToolbarLayoutSpec { + Self::TOP_PIN_BUTTON_GAP * right_control_count.saturating_sub(1) as f64 + Self::TOP_PIN_BUTTON_MARGIN_RIGHT }; - let width = left_end + gap + right_controls; + let width = (left_end + gap + right_controls).max(picker_right + gap); (width.ceil() as u32, height.ceil() as u32) } diff --git a/src/backend/wayland/toolbar/layout/top/icons.rs b/src/backend/wayland/toolbar/layout/top/icons.rs index 4562caaf..5e733f5d 100644 --- a/src/backend/wayland/toolbar/layout/top/icons.rs +++ b/src/backend/wayland/toolbar/layout/top/icons.rs @@ -2,7 +2,6 @@ use super::super::super::events::HitKind; use super::super::super::format_binding_label; use super::super::super::hit::HitRegion; use super::super::spec::ToolbarLayoutSpec; -use super::shape_buttons; use crate::config::{Action, action_label}; use crate::ui::toolbar::bindings::tool_tooltip_label; use crate::ui::toolbar::model; @@ -97,97 +96,96 @@ pub(super) fn build_hits( }); } - if model::top_text_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::EnterTextMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterTextMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterTextMode), - )), - }); - x += btn_size + gap; - } - - if model::top_sticky_note_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::EnterStickyNoteMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterStickyNoteMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterStickyNoteMode), - )), - }); - x += btn_size + gap; - } - - if model::top_screenshot_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::CaptureScreenshot, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::CaptureSelection), - snapshot - .binding_hints - .binding_for_action(Action::CaptureSelection), - )), - }); - x += btn_size + gap; - } - - if !is_simple { - if model::top_clear_canvas_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::ClearCanvas, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::ClearCanvas), - snapshot - .binding_hints - .binding_for_action(Action::ClearCanvas), - )), - }); - x += btn_size + gap; - } - - if model::top_highlight_visible(snapshot) { - let highlight_x = x; - hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::ToggleAllHighlight(!snapshot.any_highlight_active), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::ToggleHighlightTool), - snapshot - .binding_hints - .binding_for_action(Action::ToggleHighlightTool), - )), - }); - if snapshot.highlight_tool_active && model::top_highlight_ring_visible(snapshot) { - let ring_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; + for button in model::visible_top_utility_buttons(snapshot, is_simple, true) { + match button { + model::TopUtilityButton::Text => { + hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::EnterTextMode, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::EnterTextMode), + snapshot + .binding_hints + .binding_for_action(Action::EnterTextMode), + )), + }); + x += btn_size + gap; + } + model::TopUtilityButton::StickyNote => { + hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::EnterStickyNoteMode, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::EnterStickyNoteMode), + snapshot + .binding_hints + .binding_for_action(Action::EnterStickyNoteMode), + )), + }); + x += btn_size + gap; + } + model::TopUtilityButton::Screenshot => { + hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::CaptureScreenshot, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::CaptureSelection), + snapshot + .binding_hints + .binding_for_action(Action::CaptureSelection), + )), + }); + x += btn_size + gap; + } + model::TopUtilityButton::ClearCanvas => { hits.push(HitRegion { - rect: ( - highlight_x, - ring_y, - btn_size, - ToolbarLayoutSpec::TOP_ICON_FILL_HEIGHT, - ), - event: ToolbarEvent::ToggleHighlightToolRing( - !snapshot.highlight_tool_ring_enabled, - ), + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::ClearCanvas, kind: HitKind::Click, - tooltip: Some("Highlight ring".to_string()), + tooltip: Some(format_binding_label( + action_label(Action::ClearCanvas), + snapshot + .binding_hints + .binding_for_action(Action::ClearCanvas), + )), }); + x += btn_size + gap; } - x += btn_size + gap; + model::TopUtilityButton::Highlight => { + let highlight_x = x; + hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::ToggleAllHighlight(!snapshot.any_highlight_active), + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::ToggleHighlightTool), + snapshot + .binding_hints + .binding_for_action(Action::ToggleHighlightTool), + )), + }); + if snapshot.highlight_tool_active && model::top_highlight_ring_visible(snapshot) { + let ring_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; + hits.push(HitRegion { + rect: ( + highlight_x, + ring_y, + btn_size, + ToolbarLayoutSpec::TOP_ICON_FILL_HEIGHT, + ), + event: ToolbarEvent::ToggleHighlightToolRing( + !snapshot.highlight_tool_ring_enabled, + ), + kind: HitKind::Click, + tooltip: Some("Highlight ring".to_string()), + }); + } + x += btn_size + gap; + } + model::TopUtilityButton::IconMode => {} } } @@ -202,21 +200,10 @@ pub(super) fn build_hits( if snapshot.shape_picker_open && model::top_shape_picker_visible(snapshot) { let mut shape_y = y + btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - let first_row = if is_simple { - model::common_shape_tools() - } else { - shape_buttons() - }; - if model::visible_tool_count(first_row, snapshot) > 0 { - push_picker_hits(shape_y, btn_size, gap, first_row, snapshot, hits); + for row in model::visible_shape_picker_rows(snapshot, is_simple) { + push_picker_hits(shape_y, btn_size, gap, &row, snapshot, hits); shape_y += btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; } - if is_simple { - let second_row = shape_buttons(); - if model::visible_tool_count(second_row, snapshot) > 0 { - push_picker_hits(shape_y, btn_size, gap, second_row, snapshot, hits); - } - } } } diff --git a/src/backend/wayland/toolbar/layout/top/mod.rs b/src/backend/wayland/toolbar/layout/top/mod.rs index 0a2e86e3..e0350c52 100644 --- a/src/backend/wayland/toolbar/layout/top/mod.rs +++ b/src/backend/wayland/toolbar/layout/top/mod.rs @@ -3,8 +3,7 @@ use super::super::events::HitKind; use super::super::hit::HitRegion; use super::spec::ToolbarLayoutSpec; -use crate::config::ToolbarLayoutMode; -use crate::input::Tool; +use crate::config::{ToolbarLayoutMode, toolbar_item_ids as ids}; use crate::ui::toolbar::model; use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; @@ -31,7 +30,7 @@ pub fn build_top_hits( let btn_y = spec.top_pin_button_y(height); let mut right_x = width - ToolbarLayoutSpec::TOP_PIN_BUTTON_MARGIN_RIGHT - btn_size; - if model::toolbar_item_visible(snapshot, "top.chrome.close") { + if model::toolbar_item_visible(snapshot, ids::TOP_CHROME_CLOSE) { hits.push(HitRegion { rect: (right_x, btn_y, btn_size, btn_size), event: ToolbarEvent::CloseTopToolbar, @@ -41,7 +40,7 @@ pub fn build_top_hits( right_x -= btn_size + ToolbarLayoutSpec::TOP_PIN_BUTTON_GAP; } - if model::toolbar_item_visible(snapshot, "top.chrome.pin") { + if model::toolbar_item_visible(snapshot, ids::TOP_CHROME_PIN) { hits.push(HitRegion { rect: (right_x, btn_y, btn_size, btn_size), event: ToolbarEvent::PinTopToolbar(!snapshot.top_pinned), @@ -54,7 +53,3 @@ pub fn build_top_hits( }); } } - -fn shape_buttons() -> &'static [Tool] { - model::polygon_tools() -} diff --git a/src/backend/wayland/toolbar/layout/top/text.rs b/src/backend/wayland/toolbar/layout/top/text.rs index 582924d1..69d80f85 100644 --- a/src/backend/wayland/toolbar/layout/top/text.rs +++ b/src/backend/wayland/toolbar/layout/top/text.rs @@ -2,7 +2,6 @@ use super::super::super::events::HitKind; use super::super::super::format_binding_label; use super::super::super::hit::HitRegion; use super::super::spec::ToolbarLayoutSpec; -use super::shape_buttons; use crate::config::{Action, action_label}; use crate::ui::toolbar::bindings::tool_tooltip_label; use crate::ui::toolbar::model; @@ -69,61 +68,28 @@ pub(super) fn build_hits( x += fill_w + gap; } - if model::top_text_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::EnterTextMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterTextMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterTextMode), - )), - }); - x += btn_w + gap; - } - - if model::top_sticky_note_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::EnterStickyNoteMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterStickyNoteMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterStickyNoteMode), - )), - }); - x += btn_w + gap; - } - - if model::top_screenshot_visible(snapshot) { - hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::CaptureScreenshot, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::CaptureSelection), - snapshot - .binding_hints - .binding_for_action(Action::CaptureSelection), - )), - }); - x += btn_w + gap; - } - - if !is_simple && model::top_clear_canvas_visible(snapshot) { + for button in model::visible_top_utility_buttons(snapshot, is_simple, false) { + let (event, label) = match button { + model::TopUtilityButton::Text => (ToolbarEvent::EnterTextMode, Action::EnterTextMode), + model::TopUtilityButton::StickyNote => ( + ToolbarEvent::EnterStickyNoteMode, + Action::EnterStickyNoteMode, + ), + model::TopUtilityButton::Screenshot => { + (ToolbarEvent::CaptureScreenshot, Action::CaptureSelection) + } + model::TopUtilityButton::ClearCanvas => { + (ToolbarEvent::ClearCanvas, Action::ClearCanvas) + } + model::TopUtilityButton::Highlight | model::TopUtilityButton::IconMode => continue, + }; hits.push(HitRegion { rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::ClearCanvas, + event, kind: HitKind::Click, tooltip: Some(format_binding_label( - action_label(Action::ClearCanvas), - snapshot - .binding_hints - .binding_for_action(Action::ClearCanvas), + action_label(label), + snapshot.binding_hints.binding_for_action(label), )), }); x += btn_w + gap; @@ -140,21 +106,10 @@ pub(super) fn build_hits( if snapshot.shape_picker_open && model::top_shape_picker_visible(snapshot) { let mut shape_y = y + btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - let first_row = if is_simple { - model::common_shape_tools() - } else { - shape_buttons() - }; - if model::visible_tool_count(first_row, snapshot) > 0 { - push_picker_hits(shape_y, btn_w, btn_h, gap, first_row, snapshot, hits); + for row in model::visible_shape_picker_rows(snapshot, is_simple) { + push_picker_hits(shape_y, btn_w, btn_h, gap, &row, snapshot, hits); shape_y += btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; } - if is_simple { - let second_row = shape_buttons(); - if model::visible_tool_count(second_row, snapshot) > 0 { - push_picker_hits(shape_y, btn_w, btn_h, gap, second_row, snapshot, hits); - } - } } } diff --git a/src/backend/wayland/toolbar/render/side_palette/mod.rs b/src/backend/wayland/toolbar/render/side_palette/mod.rs index 992628a6..b72b4d07 100644 --- a/src/backend/wayland/toolbar/render/side_palette/mod.rs +++ b/src/backend/wayland/toolbar/render/side_palette/mod.rs @@ -20,6 +20,7 @@ use anyhow::Result; use crate::backend::wayland::toolbar::hit::HitRegion; use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; use crate::input::ToolbarDrawerTab; +use crate::ui::toolbar::model; use crate::ui::toolbar::snapshot::ToolContext; use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; @@ -117,67 +118,88 @@ pub fn render_side_palette( return Ok(()); } - // Color section: only show when the tool needs color - let colors_info = - if tool_context.needs_color && !snapshot.side_section_hidden(ToolbarSideSection::Colors) { - colors::draw_colors_section(&mut layout, &mut y) - } else { - None - }; - - // Presets section (always shown when enabled) - let hover_preset_color = if snapshot.side_section_hidden(ToolbarSideSection::Presets) { - None - } else { - presets::draw_presets_section(&mut layout, &mut y) - }; + let mut colors_info = None; + let mut hover_preset_color = None; + let mut drawer_tabs_drawn = false; + let mut thickness_block_drawn = false; + let mut text_block_drawn = false; + for section in model::ordered_side_sections(snapshot) { + match section { + ToolbarSideSection::Colors + if tool_context.needs_color + && !snapshot.side_section_hidden(ToolbarSideSection::Colors) => + { + colors_info = colors::draw_colors_section(&mut layout, &mut y); + } + ToolbarSideSection::Presets + if !snapshot.side_section_hidden(ToolbarSideSection::Presets) => + { + hover_preset_color = presets::draw_presets_section(&mut layout, &mut y); + } + ToolbarSideSection::Thickness + | ToolbarSideSection::EraserMode + | ToolbarSideSection::PolygonSides + if tool_context.needs_thickness && !thickness_block_drawn => + { + thickness_block_drawn = true; + thickness::draw_thickness_section(&mut layout, &mut y); + } + ToolbarSideSection::ArrowLabels + if tool_context.show_arrow_labels + && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) => + { + arrow::draw_arrow_section(&mut layout, &mut y); + } + ToolbarSideSection::StepMarkers + if tool_context.show_step_counter + && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) => + { + step_marker::draw_step_marker_section(&mut layout, &mut y); + } + ToolbarSideSection::MarkerOpacity + if tool_context.show_marker_opacity + && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) => + { + marker::draw_marker_opacity_section(&mut layout, &mut y); + } + ToolbarSideSection::TextSize | ToolbarSideSection::Font + if tool_context.show_font_controls && !text_block_drawn => + { + text_block_drawn = true; + text::draw_text_controls_section(&mut layout, &mut y); + } + ToolbarSideSection::Actions => { + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); + actions::draw_actions_section(&mut layout, &mut y); + } + ToolbarSideSection::Boards => { + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); + boards::draw_boards_section(&mut layout, &mut y); + } + ToolbarSideSection::Pages => { + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); + pages::draw_pages_section(&mut layout, &mut y); + } + ToolbarSideSection::StepUndo => { + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); + step::draw_step_section(&mut layout, &mut y); + } + ToolbarSideSection::Session => { + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); + session::draw_session_section(&mut layout, &mut y); + } + ToolbarSideSection::Settings => { + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); + settings::draw_settings_section(&mut layout, &mut y); + } + _ => {} + } + } + draw_drawer_tabs_once(&mut layout, &mut y, &mut drawer_tabs_drawn); if let (Some(color), Some(info)) = (hover_preset_color, &colors_info) { colors::draw_preset_hover_highlight(&layout, info, color); } - // Thickness/size slider: show when tool needs thickness - if tool_context.needs_thickness { - thickness::draw_thickness_section(&mut layout, &mut y); - } - - // Arrow labels: show when arrow tool is active - if tool_context.show_arrow_labels - && !snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) - { - arrow::draw_arrow_section(&mut layout, &mut y); - } - - // Step marker counter: show when step marker tool is active - if tool_context.show_step_counter - && !snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) - { - step_marker::draw_step_marker_section(&mut layout, &mut y); - } - - // Marker opacity: show when marker tool is active - if tool_context.show_marker_opacity - && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) - { - marker::draw_marker_opacity_section(&mut layout, &mut y); - } - - // Text controls: show when text/note mode is active - if tool_context.show_font_controls - && (!snapshot.side_section_hidden(ToolbarSideSection::TextSize) - || !snapshot.side_section_hidden(ToolbarSideSection::Font)) - { - text::draw_text_controls_section(&mut layout, &mut y); - } - - // Drawer, actions, and other sections (always available based on settings) - drawer::draw_drawer_tabs(&mut layout, &mut y); - actions::draw_actions_section(&mut layout, &mut y); - boards::draw_boards_section(&mut layout, &mut y); - pages::draw_pages_section(&mut layout, &mut y); - step::draw_step_section(&mut layout, &mut y); - session::draw_session_section(&mut layout, &mut y); - settings::draw_settings_section(&mut layout, &mut y); - draw_tooltip_with_delay( ctx, layout.hits, @@ -189,3 +211,11 @@ pub fn render_side_palette( ); Ok(()) } + +fn draw_drawer_tabs_once(layout: &mut SidePaletteLayout<'_>, y: &mut f64, drawn: &mut bool) { + if *drawn { + return; + } + drawer::draw_drawer_tabs(layout, y); + *drawn = true; +} diff --git a/src/backend/wayland/toolbar/render/side_palette/settings.rs b/src/backend/wayland/toolbar/render/side_palette/settings.rs index 31cbdedf..9cfe6497 100644 --- a/src/backend/wayland/toolbar/render/side_palette/settings.rs +++ b/src/backend/wayland/toolbar/render/side_palette/settings.rs @@ -5,8 +5,8 @@ use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; use crate::backend::wayland::toolbar::rows::{grid_layout, row_item_width}; use crate::input::ToolbarDrawerTab; use crate::toolbar_icons; -use crate::ui::toolbar::ToolbarSideSection; use crate::ui::toolbar::model::{ToolbarActivation, ToolbarIcon, ToolbarSettingsModel}; +use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection}; use crate::ui_text::UiTextStyle; use super::super::widgets::constants::{FONT_FAMILY_DEFAULT, FONT_SIZE_LABEL}; @@ -243,11 +243,30 @@ pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64) let item_hover = hover .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) .unwrap_or(false); + let order = override_item.order.as_ref(); + let order_gap = 4.0; + let handle_w = if order.is_some() { 24.0 } else { 0.0 }; + let move_btn_w = if order.is_some() { 28.0 } else { 0.0 }; + let move_buttons_w = if order.is_some() { + move_btn_w * 2.0 + order_gap * 2.0 + } else { + 0.0 + }; + if order.is_some() { + let handle_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, handle_w, item.h)) + .unwrap_or(false); + draw_button(ctx, item.x, item.y, handle_w, item.h, false, handle_hover); + draw_label_center(ctx, toggle_style, item.x, item.y, handle_w, item.h, "="); + } + let checkbox_x = item.x + handle_w + if order.is_some() { order_gap } else { 0.0 }; + let checkbox_w = + item.w - handle_w - move_buttons_w - if order.is_some() { order_gap } else { 0.0 }; draw_checkbox( ctx, - item.x, + checkbox_x, item.y, - item.w, + checkbox_w, item.h, override_item.shown, item_hover, @@ -255,11 +274,69 @@ pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64) override_item.label.as_ref(), ); hits.push(HitRegion { - rect: (item.x, item.y, item.w, item.h), + rect: (checkbox_x, item.y, checkbox_w, item.h), event: activation_event(&override_item.activation), kind: HitKind::Click, tooltip: override_item.tooltip.as_string(), }); + if let Some(order) = order { + let up_x = item.x + item.w - move_btn_w * 2.0 - order_gap; + let down_x = item.x + item.w - move_btn_w; + for (button_x, label, enabled, activation, tooltip) in [ + (up_x, "^", order.can_move_up, &order.move_up, "Move up"), + ( + down_x, + "v", + order.can_move_down, + &order.move_down, + "Move down", + ), + ] { + let button_hover = enabled + && hover + .map(|(hx, hy)| point_in_rect(hx, hy, button_x, item.y, move_btn_w, item.h)) + .unwrap_or(false); + draw_button( + ctx, + button_x, + item.y, + move_btn_w, + item.h, + false, + button_hover, + ); + draw_label_center( + ctx, + toggle_style, + button_x, + item.y, + move_btn_w, + item.h, + label, + ); + if enabled { + hits.push(HitRegion { + rect: (button_x, item.y, move_btn_w, item.h), + event: activation_event(activation), + kind: HitKind::Click, + tooltip: Some(format!("{} {}", tooltip, override_item.label)), + }); + } + } + hits.push(HitRegion { + rect: (item.x, item.y, item.w, item.h), + event: ToolbarEvent::StartToolbarItemDrag { + group: order.group, + id: override_item.id, + }, + kind: HitKind::DragToolbarItem { + group: order.group, + id: override_item.id, + target_index: order.index, + }, + tooltip: Some(format!("Drag {} to reorder", override_item.label)), + }); + } } *y += settings_card_h + section_gap; diff --git a/src/backend/wayland/toolbar/render/top_strip/icons/shape_picker.rs b/src/backend/wayland/toolbar/render/top_strip/icons/shape_picker.rs index 7941c292..997f8c58 100644 --- a/src/backend/wayland/toolbar/render/top_strip/icons/shape_picker.rs +++ b/src/backend/wayland/toolbar/render/top_strip/icons/shape_picker.rs @@ -22,21 +22,10 @@ pub(super) fn draw_shape_picker_row( return; } - let first_row = if is_simple { - model::common_shape_tools() - } else { - model::polygon_tools() - }; - if model::visible_tool_count(first_row, layout.snapshot) > 0 { - draw_picker_row(layout, handle_w, shape_y, btn_size, icon_size, first_row); + for row in model::visible_shape_picker_rows(layout.snapshot, is_simple) { + draw_picker_row(layout, handle_w, shape_y, btn_size, icon_size, &row); shape_y += btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; } - if is_simple { - let second_row = model::polygon_tools(); - if model::visible_tool_count(second_row, layout.snapshot) > 0 { - draw_picker_row(layout, handle_w, shape_y, btn_size, icon_size, second_row); - } - } } fn draw_picker_row( diff --git a/src/backend/wayland/toolbar/render/top_strip/icons/utility_row.rs b/src/backend/wayland/toolbar/render/top_strip/icons/utility_row.rs index 5600472c..624a2723 100644 --- a/src/backend/wayland/toolbar/render/top_strip/icons/utility_row.rs +++ b/src/backend/wayland/toolbar/render/top_strip/icons/utility_row.rs @@ -30,190 +30,198 @@ pub(super) fn draw_utility_row( size: FONT_SIZE_SMALL, }; - if model::top_text_visible(snapshot) { - let is_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - layout.ctx, - x, - y, - btn_size, - btn_size, - snapshot.text_active, - is_hover, - ); - set_icon_color(layout.ctx, is_hover); - toolbar_icons::draw_icon_text( - layout.ctx, - x + (btn_size - icon_size) / 2.0, - y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::EnterTextMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterTextMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterTextMode), - )), - }); - x += btn_size + gap; - } + for button in model::visible_top_utility_buttons(snapshot, is_simple, true) { + match button { + model::TopUtilityButton::Text => { + let is_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) + .unwrap_or(false); + draw_button( + layout.ctx, + x, + y, + btn_size, + btn_size, + snapshot.text_active, + is_hover, + ); + set_icon_color(layout.ctx, is_hover); + toolbar_icons::draw_icon_text( + layout.ctx, + x + (btn_size - icon_size) / 2.0, + y + (btn_size - icon_size) / 2.0, + icon_size, + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::EnterTextMode, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::EnterTextMode), + snapshot + .binding_hints + .binding_for_action(Action::EnterTextMode), + )), + }); + x += btn_size + gap; + } - if model::top_sticky_note_visible(snapshot) { - let note_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - layout.ctx, - x, - y, - btn_size, - btn_size, - snapshot.note_active, - note_hover, - ); - set_icon_color(layout.ctx, note_hover); - toolbar_icons::draw_icon_note( - layout.ctx, - x + (btn_size - icon_size) / 2.0, - y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::EnterStickyNoteMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterStickyNoteMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterStickyNoteMode), - )), - }); - x += btn_size + gap; - } + model::TopUtilityButton::StickyNote => { + let note_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) + .unwrap_or(false); + draw_button( + layout.ctx, + x, + y, + btn_size, + btn_size, + snapshot.note_active, + note_hover, + ); + set_icon_color(layout.ctx, note_hover); + toolbar_icons::draw_icon_note( + layout.ctx, + x + (btn_size - icon_size) / 2.0, + y + (btn_size - icon_size) / 2.0, + icon_size, + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::EnterStickyNoteMode, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::EnterStickyNoteMode), + snapshot + .binding_hints + .binding_for_action(Action::EnterStickyNoteMode), + )), + }); + x += btn_size + gap; + } - if !is_simple && model::top_clear_canvas_visible(snapshot) { - let clear_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) - .unwrap_or(false); - draw_button(layout.ctx, x, y, btn_size, btn_size, false, clear_hover); - set_icon_color(layout.ctx, clear_hover); - toolbar_icons::draw_icon_clear( - layout.ctx, - x + (btn_size - icon_size) / 2.0, - y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::ClearCanvas, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::ClearCanvas), - snapshot - .binding_hints - .binding_for_action(Action::ClearCanvas), - )), - }); - x += btn_size + gap; - } + model::TopUtilityButton::ClearCanvas => { + let clear_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) + .unwrap_or(false); + draw_button(layout.ctx, x, y, btn_size, btn_size, false, clear_hover); + set_icon_color(layout.ctx, clear_hover); + toolbar_icons::draw_icon_clear( + layout.ctx, + x + (btn_size - icon_size) / 2.0, + y + (btn_size - icon_size) / 2.0, + icon_size, + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::ClearCanvas, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::ClearCanvas), + snapshot + .binding_hints + .binding_for_action(Action::ClearCanvas), + )), + }); + x += btn_size + gap; + } - if model::top_screenshot_visible(snapshot) { - let screenshot_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - layout.ctx, - x, - y, - btn_size, - btn_size, - false, - screenshot_hover, - ); - set_icon_color(layout.ctx, screenshot_hover); - toolbar_icons::draw_icon_screenshot( - layout.ctx, - x + (btn_size - icon_size) / 2.0, - y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::CaptureScreenshot, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::CaptureSelection), - snapshot - .binding_hints - .binding_for_action(Action::CaptureSelection), - )), - }); - x += btn_size + gap; - } + model::TopUtilityButton::Screenshot => { + let screenshot_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) + .unwrap_or(false); + draw_button( + layout.ctx, + x, + y, + btn_size, + btn_size, + false, + screenshot_hover, + ); + set_icon_color(layout.ctx, screenshot_hover); + toolbar_icons::draw_icon_screenshot( + layout.ctx, + x + (btn_size - icon_size) / 2.0, + y + (btn_size - icon_size) / 2.0, + icon_size, + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::CaptureScreenshot, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::CaptureSelection), + snapshot + .binding_hints + .binding_for_action(Action::CaptureSelection), + )), + }); + x += btn_size + gap; + } + + model::TopUtilityButton::Highlight => { + let highlight_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) + .unwrap_or(false); + draw_button( + layout.ctx, + x, + y, + btn_size, + btn_size, + snapshot.any_highlight_active, + highlight_hover, + ); + set_icon_color(layout.ctx, highlight_hover); + toolbar_icons::draw_icon_highlight( + layout.ctx, + x + (btn_size - icon_size) / 2.0, + y + (btn_size - icon_size) / 2.0, + icon_size, + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_size, btn_size), + event: ToolbarEvent::ToggleAllHighlight(!snapshot.any_highlight_active), + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::ToggleHighlightTool), + snapshot + .binding_hints + .binding_for_action(Action::ToggleHighlightTool), + )), + }); + if snapshot.highlight_tool_active && model::top_highlight_ring_visible(snapshot) { + let ring_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; + let ring_h = ToolbarLayoutSpec::TOP_ICON_FILL_HEIGHT; + let ring_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, ring_y, btn_size, ring_h)) + .unwrap_or(false); + draw_mini_checkbox( + layout.ctx, + x, + ring_y, + btn_size, + ring_h, + snapshot.highlight_tool_ring_enabled, + ring_hover, + mini_label_style, + "Ring", + ); + layout.hits.push(HitRegion { + rect: (x, ring_y, btn_size, ring_h), + event: ToolbarEvent::ToggleHighlightToolRing( + !snapshot.highlight_tool_ring_enabled, + ), + kind: HitKind::Click, + tooltip: Some("Highlight ring".to_string()), + }); + } + x += btn_size + gap; + } - if !is_simple && model::top_highlight_visible(snapshot) { - let highlight_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - layout.ctx, - x, - y, - btn_size, - btn_size, - snapshot.any_highlight_active, - highlight_hover, - ); - set_icon_color(layout.ctx, highlight_hover); - toolbar_icons::draw_icon_highlight( - layout.ctx, - x + (btn_size - icon_size) / 2.0, - y + (btn_size - icon_size) / 2.0, - icon_size, - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::ToggleAllHighlight(!snapshot.any_highlight_active), - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::ToggleHighlightTool), - snapshot - .binding_hints - .binding_for_action(Action::ToggleHighlightTool), - )), - }); - if snapshot.highlight_tool_active && model::top_highlight_ring_visible(snapshot) { - let ring_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; - let ring_h = ToolbarLayoutSpec::TOP_ICON_FILL_HEIGHT; - let ring_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, ring_y, btn_size, ring_h)) - .unwrap_or(false); - draw_mini_checkbox( - layout.ctx, - x, - ring_y, - btn_size, - ring_h, - snapshot.highlight_tool_ring_enabled, - ring_hover, - mini_label_style, - "Ring", - ); - layout.hits.push(HitRegion { - rect: (x, ring_y, btn_size, ring_h), - event: ToolbarEvent::ToggleHighlightToolRing(!snapshot.highlight_tool_ring_enabled), - kind: HitKind::Click, - tooltip: Some("Highlight ring".to_string()), - }); + model::TopUtilityButton::IconMode => {} } - x += btn_size + gap; } x diff --git a/src/backend/wayland/toolbar/render/top_strip/mod.rs b/src/backend/wayland/toolbar/render/top_strip/mod.rs index 42c3c4e8..91b193b8 100644 --- a/src/backend/wayland/toolbar/render/top_strip/mod.rs +++ b/src/backend/wayland/toolbar/render/top_strip/mod.rs @@ -8,6 +8,7 @@ use anyhow::Result; use crate::backend::wayland::toolbar::format_binding_label; use crate::backend::wayland::toolbar::hit::HitRegion; use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; +use crate::config::toolbar_item_ids as ids; use crate::input::Tool; use crate::ui::toolbar::ToolbarSnapshot; @@ -81,7 +82,7 @@ pub fn render_top_strip( let handle_w = ToolbarLayoutSpec::TOP_HANDLE_SIZE; let handle_h = ToolbarLayoutSpec::TOP_HANDLE_SIZE; let handle_y = ToolbarLayoutSpec::TOP_HANDLE_Y; - let handle_visible = model::toolbar_item_visible(snapshot, "top.chrome.drag"); + let handle_visible = model::toolbar_item_visible(snapshot, ids::TOP_CHROME_DRAG); if handle_visible { let handle_hover = layout .hover @@ -126,7 +127,7 @@ pub fn render_top_strip( let btn_size = ToolbarLayoutSpec::TOP_PIN_BUTTON_SIZE; let btn_y = layout.spec.top_pin_button_y(height); let mut right_x = width - ToolbarLayoutSpec::TOP_PIN_BUTTON_MARGIN_RIGHT - btn_size; - if model::toolbar_item_visible(snapshot, "top.chrome.close") { + if model::toolbar_item_visible(snapshot, ids::TOP_CHROME_CLOSE) { let close_hover = layout .hover .map(|(hx, hy)| point_in_rect(hx, hy, right_x, btn_y, btn_size, btn_size)) @@ -141,7 +142,7 @@ pub fn render_top_strip( right_x -= btn_size + ToolbarLayoutSpec::TOP_PIN_BUTTON_GAP; } - if model::toolbar_item_visible(snapshot, "top.chrome.pin") { + if model::toolbar_item_visible(snapshot, ids::TOP_CHROME_PIN) { let pin_hover = layout .hover .map(|(hx, hy)| point_in_rect(hx, hy, right_x, btn_y, btn_size, btn_size)) diff --git a/src/backend/wayland/toolbar/render/top_strip/text.rs b/src/backend/wayland/toolbar/render/top_strip/text.rs index c39206d5..f00a5c29 100644 --- a/src/backend/wayland/toolbar/render/top_strip/text.rs +++ b/src/backend/wayland/toolbar/render/top_strip/text.rs @@ -121,108 +121,110 @@ pub(super) fn draw_text_strip( x += fill_w + gap; } - if model::top_text_visible(snapshot) { - let is_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) - .unwrap_or(false); - draw_button(ctx, x, y, btn_w, btn_h, snapshot.text_active, is_hover); - draw_label_center( - ctx, - label_style, - x, - y, - btn_w, - btn_h, - action_short_label(Action::EnterTextMode), - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::EnterTextMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterTextMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterTextMode), - )), - }); - x += btn_w + gap; - } - - if model::top_sticky_note_visible(snapshot) { - let note_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) - .unwrap_or(false); - draw_button(ctx, x, y, btn_w, btn_h, snapshot.note_active, note_hover); - draw_label_center( - ctx, - label_style, - x, - y, - btn_w, - btn_h, - action_short_label(Action::EnterStickyNoteMode), - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::EnterStickyNoteMode, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::EnterStickyNoteMode), - snapshot - .binding_hints - .binding_for_action(Action::EnterStickyNoteMode), - )), - }); - x += btn_w + gap; - } - - if model::top_screenshot_visible(snapshot) { - let screenshot_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) - .unwrap_or(false); - draw_button(ctx, x, y, btn_w, btn_h, false, screenshot_hover); - draw_label_center(ctx, label_style, x, y, btn_w, btn_h, "Shot"); - layout.hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::CaptureScreenshot, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::CaptureSelection), - snapshot - .binding_hints - .binding_for_action(Action::CaptureSelection), - )), - }); - x += btn_w + gap; - } - - if !is_simple && model::top_clear_canvas_visible(snapshot) { - let clear_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) - .unwrap_or(false); - draw_button(ctx, x, y, btn_w, btn_h, false, clear_hover); - draw_label_center( - ctx, - label_style, - x, - y, - btn_w, - btn_h, - action_short_label(Action::ClearCanvas), - ); - layout.hits.push(HitRegion { - rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::ClearCanvas, - kind: HitKind::Click, - tooltip: Some(format_binding_label( - action_label(Action::ClearCanvas), - snapshot - .binding_hints - .binding_for_action(Action::ClearCanvas), - )), - }); - x += btn_w + gap; + for button in model::visible_top_utility_buttons(snapshot, is_simple, false) { + match button { + model::TopUtilityButton::Text => { + let is_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) + .unwrap_or(false); + draw_button(ctx, x, y, btn_w, btn_h, snapshot.text_active, is_hover); + draw_label_center( + ctx, + label_style, + x, + y, + btn_w, + btn_h, + action_short_label(Action::EnterTextMode), + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_w, btn_h), + event: ToolbarEvent::EnterTextMode, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::EnterTextMode), + snapshot + .binding_hints + .binding_for_action(Action::EnterTextMode), + )), + }); + x += btn_w + gap; + } + model::TopUtilityButton::StickyNote => { + let note_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) + .unwrap_or(false); + draw_button(ctx, x, y, btn_w, btn_h, snapshot.note_active, note_hover); + draw_label_center( + ctx, + label_style, + x, + y, + btn_w, + btn_h, + action_short_label(Action::EnterStickyNoteMode), + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_w, btn_h), + event: ToolbarEvent::EnterStickyNoteMode, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::EnterStickyNoteMode), + snapshot + .binding_hints + .binding_for_action(Action::EnterStickyNoteMode), + )), + }); + x += btn_w + gap; + } + model::TopUtilityButton::Screenshot => { + let screenshot_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) + .unwrap_or(false); + draw_button(ctx, x, y, btn_w, btn_h, false, screenshot_hover); + draw_label_center(ctx, label_style, x, y, btn_w, btn_h, "Shot"); + layout.hits.push(HitRegion { + rect: (x, y, btn_w, btn_h), + event: ToolbarEvent::CaptureScreenshot, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::CaptureSelection), + snapshot + .binding_hints + .binding_for_action(Action::CaptureSelection), + )), + }); + x += btn_w + gap; + } + model::TopUtilityButton::ClearCanvas => { + let clear_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) + .unwrap_or(false); + draw_button(ctx, x, y, btn_w, btn_h, false, clear_hover); + draw_label_center( + ctx, + label_style, + x, + y, + btn_w, + btn_h, + action_short_label(Action::ClearCanvas), + ); + layout.hits.push(HitRegion { + rect: (x, y, btn_w, btn_h), + event: ToolbarEvent::ClearCanvas, + kind: HitKind::Click, + tooltip: Some(format_binding_label( + action_label(Action::ClearCanvas), + snapshot + .binding_hints + .binding_for_action(Action::ClearCanvas), + )), + }); + x += btn_w + gap; + } + model::TopUtilityButton::Highlight | model::TopUtilityButton::IconMode => {} + } } if model::top_icon_mode_toggle_visible(snapshot) { @@ -263,37 +265,10 @@ pub(super) fn draw_text_strip( if snapshot.shape_picker_open && model::top_shape_picker_visible(snapshot) { let mut shape_y = y + btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - let first_row = if is_simple { - model::common_shape_tools() - } else { - model::polygon_tools() - }; - if model::visible_tool_count(first_row, snapshot) > 0 { - draw_picker_text_row( - layout, - handle_w, - shape_y, - btn_w, - btn_h, - label_style, - first_row, - ); + for row in model::visible_shape_picker_rows(snapshot, is_simple) { + draw_picker_text_row(layout, handle_w, shape_y, btn_w, btn_h, label_style, &row); shape_y += btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; } - if is_simple { - let second_row = model::polygon_tools(); - if model::visible_tool_count(second_row, snapshot) > 0 { - draw_picker_text_row( - layout, - handle_w, - shape_y, - btn_w, - btn_h, - label_style, - second_row, - ); - } - } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index cb6c56e1..d543f600 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -46,8 +46,10 @@ pub use types::{ RenderColorMappingConfig, RenderProfileConfig, RenderProfileExportMode, RenderProfilesConfig, ResolvedToolbarItems, SessionCompression, SessionConfig, SessionStorageMode, StatusBarStyle, ToolPresetConfig, ToolbarConfig, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, - ToolbarItemId, ToolbarItemSurface, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverride, - ToolbarModeOverrides, UiConfig, toolbar_item_definitions, validate_pdf_label_template, + ToolbarItemId, ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, + ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverride, ToolbarModeOverrides, UiConfig, + toolbar_item_definitions, toolbar_item_ids, toolbar_item_order_group, + validate_pdf_label_template, }; #[cfg(tablet)] #[allow(unused_imports)] diff --git a/src/config/types/mod.rs b/src/config/types/mod.rs index d682ffb1..6372dc17 100644 --- a/src/config/types/mod.rs +++ b/src/config/types/mod.rs @@ -48,11 +48,13 @@ pub use session::{SessionCompression, SessionConfig, SessionStorageMode}; pub use status_bar::StatusBarStyle; #[cfg(tablet)] pub use tablet::{StylusButtonBinding, TabletInputConfig}; +pub use toolbar::ids as toolbar_item_ids; #[allow(unused_imports)] pub use toolbar::{ ResolvedToolbarItems, ToolbarConfig, ToolbarGroupId, ToolbarItemCategory, - ToolbarItemDefinition, ToolbarItemId, ToolbarItemSurface, ToolbarItemsConfig, - ToolbarLayoutMode, ToolbarModeOverride, ToolbarModeOverrides, ToolbarSectionDefaults, - toolbar_item_definitions, + ToolbarItemDefinition, ToolbarItemId, ToolbarItemOrderConfig, ToolbarItemOrderGroup, + ToolbarItemSurface, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverride, + ToolbarModeOverrides, ToolbarSectionDefaults, toolbar_item_definitions, + toolbar_item_order_group, }; pub use ui::UiConfig; diff --git a/src/config/types/toolbar/ids.rs b/src/config/types/toolbar/ids.rs new file mode 100644 index 00000000..d673f7d3 --- /dev/null +++ b/src/config/types/toolbar/ids.rs @@ -0,0 +1,149 @@ +use super::items::ToolbarItemId; + +pub const TOP_CHROME_DRAG: ToolbarItemId = ToolbarItemId::from_known("top.chrome.drag"); +pub const TOP_CHROME_PIN: ToolbarItemId = ToolbarItemId::from_known("top.chrome.pin"); +pub const TOP_CHROME_CLOSE: ToolbarItemId = ToolbarItemId::from_known("top.chrome.close"); + +pub const TOP_TOOL_SELECT: ToolbarItemId = ToolbarItemId::from_known("top.tool.select"); +pub const TOP_TOOL_PEN: ToolbarItemId = ToolbarItemId::from_known("top.tool.pen"); +pub const TOP_TOOL_MARKER: ToolbarItemId = ToolbarItemId::from_known("top.tool.marker"); +pub const TOP_TOOL_STEP_MARKER: ToolbarItemId = ToolbarItemId::from_known("top.tool.step-marker"); +pub const TOP_TOOL_ERASER: ToolbarItemId = ToolbarItemId::from_known("top.tool.eraser"); +pub const TOP_TOOL_LINE: ToolbarItemId = ToolbarItemId::from_known("top.tool.line"); +pub const TOP_TOOL_RECT: ToolbarItemId = ToolbarItemId::from_known("top.tool.rect"); +pub const TOP_TOOL_ELLIPSE: ToolbarItemId = ToolbarItemId::from_known("top.tool.ellipse"); +pub const TOP_TOOL_ARROW: ToolbarItemId = ToolbarItemId::from_known("top.tool.arrow"); +pub const TOP_TOOL_BLUR: ToolbarItemId = ToolbarItemId::from_known("top.tool.blur"); +pub const TOP_TOOL_TRIANGLE: ToolbarItemId = ToolbarItemId::from_known("top.tool.triangle"); +pub const TOP_TOOL_PARALLELOGRAM: ToolbarItemId = + ToolbarItemId::from_known("top.tool.parallelogram"); +pub const TOP_TOOL_RHOMBUS: ToolbarItemId = ToolbarItemId::from_known("top.tool.rhombus"); +pub const TOP_TOOL_REGULAR_POLYGON: ToolbarItemId = + ToolbarItemId::from_known("top.tool.regular-polygon"); +pub const TOP_TOOL_FREEFORM_POLYGON: ToolbarItemId = + ToolbarItemId::from_known("top.tool.freeform-polygon"); + +pub const TOP_UTILITY_SHAPE_PICKER: ToolbarItemId = + ToolbarItemId::from_known("top.utility.shape-picker"); +pub const TOP_UTILITY_FILL: ToolbarItemId = ToolbarItemId::from_known("top.utility.fill"); +pub const TOP_UTILITY_TEXT: ToolbarItemId = ToolbarItemId::from_known("top.utility.text"); +pub const TOP_UTILITY_STICKY_NOTE: ToolbarItemId = + ToolbarItemId::from_known("top.utility.sticky-note"); +pub const TOP_UTILITY_CLEAR_CANVAS: ToolbarItemId = + ToolbarItemId::from_known("top.utility.clear-canvas"); +pub const TOP_UTILITY_SCREENSHOT: ToolbarItemId = + ToolbarItemId::from_known("top.utility.screenshot"); +pub const TOP_UTILITY_HIGHLIGHT: ToolbarItemId = ToolbarItemId::from_known("top.utility.highlight"); +pub const TOP_UTILITY_HIGHLIGHT_RING: ToolbarItemId = + ToolbarItemId::from_known("top.utility.highlight-ring"); +pub const TOP_UTILITY_ICON_MODE_ICONS: ToolbarItemId = + ToolbarItemId::from_known("top.utility.icon-mode-icons"); +pub const TOP_UTILITY_ICON_MODE_TEXT: ToolbarItemId = + ToolbarItemId::from_known("top.utility.icon-mode-text"); + +pub const SIDE_GROUP_COLORS: ToolbarItemId = ToolbarItemId::from_known("side.group.colors"); +pub const SIDE_GROUP_THICKNESS: ToolbarItemId = ToolbarItemId::from_known("side.group.thickness"); +pub const SIDE_GROUP_ERASER_MODE: ToolbarItemId = + ToolbarItemId::from_known("side.group.eraser-mode"); +pub const SIDE_GROUP_POLYGON_SIDES: ToolbarItemId = + ToolbarItemId::from_known("side.group.polygon-sides"); +pub const SIDE_GROUP_ARROW_LABELS: ToolbarItemId = + ToolbarItemId::from_known("side.group.arrow-labels"); +pub const SIDE_GROUP_STEP_MARKERS: ToolbarItemId = + ToolbarItemId::from_known("side.group.step-markers"); +pub const SIDE_GROUP_STEP_UNDO: ToolbarItemId = ToolbarItemId::from_known("side.group.step-undo"); +pub const SIDE_GROUP_MARKER_OPACITY: ToolbarItemId = + ToolbarItemId::from_known("side.group.marker-opacity"); +pub const SIDE_GROUP_TEXT_SIZE: ToolbarItemId = ToolbarItemId::from_known("side.group.text-size"); +pub const SIDE_GROUP_FONT: ToolbarItemId = ToolbarItemId::from_known("side.group.font"); +pub const SIDE_GROUP_ACTIONS: ToolbarItemId = ToolbarItemId::from_known("side.group.actions"); +pub const SIDE_GROUP_PAGES: ToolbarItemId = ToolbarItemId::from_known("side.group.pages"); +pub const SIDE_GROUP_BOARDS: ToolbarItemId = ToolbarItemId::from_known("side.group.boards"); +pub const SIDE_GROUP_PRESETS: ToolbarItemId = ToolbarItemId::from_known("side.group.presets"); +pub const SIDE_GROUP_SETTINGS: ToolbarItemId = ToolbarItemId::from_known("side.group.settings"); +pub const SIDE_GROUP_SESSION: ToolbarItemId = ToolbarItemId::from_known("side.group.session"); + +pub const SIDE_ACTIONS_UNDO: ToolbarItemId = ToolbarItemId::from_known("side.actions.undo"); +pub const SIDE_ACTIONS_REDO: ToolbarItemId = ToolbarItemId::from_known("side.actions.redo"); +pub const SIDE_ACTIONS_CLEAR_CANVAS: ToolbarItemId = + ToolbarItemId::from_known("side.actions.clear-canvas"); +pub const SIDE_ACTIONS_ZOOM_IN: ToolbarItemId = ToolbarItemId::from_known("side.actions.zoom-in"); +pub const SIDE_ACTIONS_ZOOM_OUT: ToolbarItemId = ToolbarItemId::from_known("side.actions.zoom-out"); +pub const SIDE_ACTIONS_RESET_ZOOM: ToolbarItemId = + ToolbarItemId::from_known("side.actions.reset-zoom"); +pub const SIDE_ACTIONS_TOGGLE_ZOOM_LOCK: ToolbarItemId = + ToolbarItemId::from_known("side.actions.toggle-zoom-lock"); +pub const SIDE_ACTIONS_UNDO_ALL: ToolbarItemId = ToolbarItemId::from_known("side.actions.undo-all"); +pub const SIDE_ACTIONS_REDO_ALL: ToolbarItemId = ToolbarItemId::from_known("side.actions.redo-all"); +pub const SIDE_ACTIONS_UNDO_ALL_DELAYED: ToolbarItemId = + ToolbarItemId::from_known("side.actions.undo-all-delayed"); +pub const SIDE_ACTIONS_REDO_ALL_DELAYED: ToolbarItemId = + ToolbarItemId::from_known("side.actions.redo-all-delayed"); +pub const SIDE_ACTIONS_FREEZE: ToolbarItemId = ToolbarItemId::from_known("side.actions.freeze"); + +pub const SIDE_PAGES_PREVIOUS: ToolbarItemId = ToolbarItemId::from_known("side.pages.previous"); +pub const SIDE_PAGES_NEXT: ToolbarItemId = ToolbarItemId::from_known("side.pages.next"); +pub const SIDE_PAGES_NEW: ToolbarItemId = ToolbarItemId::from_known("side.pages.new"); +pub const SIDE_PAGES_DUPLICATE: ToolbarItemId = ToolbarItemId::from_known("side.pages.duplicate"); +pub const SIDE_PAGES_DELETE: ToolbarItemId = ToolbarItemId::from_known("side.pages.delete"); + +pub const SIDE_BOARDS_PREVIOUS: ToolbarItemId = ToolbarItemId::from_known("side.boards.previous"); +pub const SIDE_BOARDS_NEXT: ToolbarItemId = ToolbarItemId::from_known("side.boards.next"); +pub const SIDE_BOARDS_NEW: ToolbarItemId = ToolbarItemId::from_known("side.boards.new"); +pub const SIDE_BOARDS_DUPLICATE: ToolbarItemId = ToolbarItemId::from_known("side.boards.duplicate"); +pub const SIDE_BOARDS_DELETE: ToolbarItemId = ToolbarItemId::from_known("side.boards.delete"); +pub const SIDE_BOARDS_RENAME: ToolbarItemId = ToolbarItemId::from_known("side.boards.rename"); + +pub const SIDE_SETTINGS_CONTEXT_AWARE_UI: ToolbarItemId = + ToolbarItemId::from_known("side.settings.context-aware-ui"); +pub const SIDE_SETTINGS_TEXT_CONTROLS: ToolbarItemId = + ToolbarItemId::from_known("side.settings.text-controls"); +pub const SIDE_SETTINGS_STATUS_BAR: ToolbarItemId = + ToolbarItemId::from_known("side.settings.status-bar"); +pub const SIDE_SETTINGS_STATUS_BOARD_BADGE: ToolbarItemId = + ToolbarItemId::from_known("side.settings.status-board-badge"); +pub const SIDE_SETTINGS_STATUS_PAGE_BADGE: ToolbarItemId = + ToolbarItemId::from_known("side.settings.status-page-badge"); +pub const SIDE_SETTINGS_FLOATING_BADGE_ALWAYS: ToolbarItemId = + ToolbarItemId::from_known("side.settings.floating-badge-always"); +pub const SIDE_SETTINGS_PRESET_TOASTS: ToolbarItemId = + ToolbarItemId::from_known("side.settings.preset-toasts"); +pub const SIDE_SETTINGS_PRESETS: ToolbarItemId = ToolbarItemId::from_known("side.settings.presets"); +pub const SIDE_SETTINGS_ACTIONS: ToolbarItemId = ToolbarItemId::from_known("side.settings.actions"); +pub const SIDE_SETTINGS_ZOOM_ACTIONS: ToolbarItemId = + ToolbarItemId::from_known("side.settings.zoom-actions"); +pub const SIDE_SETTINGS_ADVANCED_ACTIONS: ToolbarItemId = + ToolbarItemId::from_known("side.settings.advanced-actions"); +pub const SIDE_SETTINGS_BOARDS: ToolbarItemId = ToolbarItemId::from_known("side.settings.boards"); +pub const SIDE_SETTINGS_PAGES: ToolbarItemId = ToolbarItemId::from_known("side.settings.pages"); +pub const SIDE_SETTINGS_STEP_CONTROLS: ToolbarItemId = + ToolbarItemId::from_known("side.settings.step-controls"); +pub const SIDE_SETTINGS_CONFIGURATOR: ToolbarItemId = + ToolbarItemId::from_known("side.settings.configurator"); +pub const SIDE_SETTINGS_CONFIG_FILE: ToolbarItemId = + ToolbarItemId::from_known("side.settings.config-file"); + +pub const SIDE_SESSION_OPEN: ToolbarItemId = ToolbarItemId::from_known("side.session.open"); +pub const SIDE_SESSION_SAVE_AS: ToolbarItemId = ToolbarItemId::from_known("side.session.save-as"); +pub const SIDE_SESSION_INFO: ToolbarItemId = ToolbarItemId::from_known("side.session.info"); +pub const SIDE_SESSION_CLEAR: ToolbarItemId = ToolbarItemId::from_known("side.session.clear"); +pub const SIDE_SESSION_MANAGER: ToolbarItemId = ToolbarItemId::from_known("side.session.manager"); + +pub const SIDE_TOOL_OPTIONS_COLOR: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.color"); +pub const SIDE_TOOL_OPTIONS_THICKNESS: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.thickness"); +pub const SIDE_TOOL_OPTIONS_MARKER_OPACITY: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.marker-opacity"); +pub const SIDE_TOOL_OPTIONS_ERASER_MODE: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.eraser-mode"); +pub const SIDE_TOOL_OPTIONS_FONT_SIZE: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.font-size"); +pub const SIDE_TOOL_OPTIONS_FONT_FAMILY: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.font-family"); +pub const SIDE_TOOL_OPTIONS_POLYGON_SIDES: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.polygon-sides"); +pub const SIDE_TOOL_OPTIONS_ARROW_LABELS: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.arrow-labels"); +pub const SIDE_TOOL_OPTIONS_STEP_MARKER_RESET: ToolbarItemId = + ToolbarItemId::from_known("side.tool-options.step-marker-reset"); diff --git a/src/config/types/toolbar/items.rs b/src/config/types/toolbar/items.rs index d545331f..ed14e46f 100644 --- a/src/config/types/toolbar/items.rs +++ b/src/config/types/toolbar/items.rs @@ -4,6 +4,8 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; +use super::ids; + /// User-authored item-level toolbar customization. /// /// The raw strings are intentionally preserved so unknown IDs from future @@ -13,17 +15,62 @@ use serde::{Deserialize, Serialize}; pub struct ToolbarItemsConfig { #[serde(default)] pub hidden: Vec, + #[serde(default)] + pub order: ToolbarItemOrderConfig, } -const DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS: &[&str] = &["top.utility.screenshot"]; +const DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS: &[ToolbarItemId] = &[ids::TOP_UTILITY_SCREENSHOT]; + +const DEFAULT_TOP_TOOLS_ORDER: &[ToolbarItemId] = &[ + ids::TOP_TOOL_SELECT, + ids::TOP_TOOL_PEN, + ids::TOP_TOOL_MARKER, + ids::TOP_TOOL_STEP_MARKER, + ids::TOP_TOOL_ERASER, + ids::TOP_TOOL_LINE, + ids::TOP_TOOL_RECT, + ids::TOP_TOOL_ELLIPSE, + ids::TOP_TOOL_ARROW, + ids::TOP_TOOL_BLUR, + ids::TOP_TOOL_TRIANGLE, + ids::TOP_TOOL_PARALLELOGRAM, + ids::TOP_TOOL_RHOMBUS, + ids::TOP_TOOL_REGULAR_POLYGON, + ids::TOP_TOOL_FREEFORM_POLYGON, +]; + +const DEFAULT_TOP_CONTROLS_ORDER: &[ToolbarItemId] = &[ + ids::TOP_UTILITY_TEXT, + ids::TOP_UTILITY_STICKY_NOTE, + ids::TOP_UTILITY_SCREENSHOT, + ids::TOP_UTILITY_CLEAR_CANVAS, + ids::TOP_UTILITY_HIGHLIGHT, +]; + +const DEFAULT_SIDE_SECTIONS_ORDER: &[ToolbarItemId] = &[ + ids::SIDE_GROUP_COLORS, + ids::SIDE_GROUP_PRESETS, + ids::SIDE_GROUP_THICKNESS, + ids::SIDE_GROUP_ARROW_LABELS, + ids::SIDE_GROUP_STEP_MARKERS, + ids::SIDE_GROUP_MARKER_OPACITY, + ids::SIDE_GROUP_TEXT_SIZE, + ids::SIDE_GROUP_ACTIONS, + ids::SIDE_GROUP_BOARDS, + ids::SIDE_GROUP_PAGES, + ids::SIDE_GROUP_STEP_UNDO, + ids::SIDE_GROUP_SESSION, + ids::SIDE_GROUP_SETTINGS, +]; impl Default for ToolbarItemsConfig { fn default() -> Self { Self { hidden: DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS .iter() - .map(|id| (*id).to_string()) + .map(|id| id.as_str().to_string()) .collect(), + order: ToolbarItemOrderConfig::default(), } } } @@ -45,6 +92,7 @@ impl ToolbarItemsConfig { ResolvedToolbarItems { hidden, unknown_hidden, + order: self.order.resolved(), } } @@ -76,7 +124,7 @@ impl ToolbarItemsConfig { let original = self.hidden.clone(); let mut next: Vec = DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS .iter() - .map(|id| (*id).to_string()) + .map(|id| id.as_str().to_string()) .collect(); for raw in self.hidden.drain(..) { @@ -89,12 +137,57 @@ impl ToolbarItemsConfig { self.hidden = next; changed } + + pub fn move_item_by( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + delta: isize, + ) -> bool { + let order = self.order.resolved().ordered_ids(group).to_vec(); + let Some(current) = order.iter().position(|candidate| *candidate == id) else { + return false; + }; + let target = current + .saturating_add_signed(delta) + .min(order.len().saturating_sub(1)); + self.move_item_to_index(group, id, target) + } + + pub fn move_item_to_index( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + target_index: usize, + ) -> bool { + if !toolbar_item_id_in_order_group(id, group) { + return false; + } + + let mut order = self.order.resolved().ordered_ids(group).to_vec(); + let Some(current) = order.iter().position(|candidate| *candidate == id) else { + return false; + }; + let item = order.remove(current); + let target = target_index.min(order.len()); + order.insert(target, item); + self.set_known_order(group, &order) + } + + pub fn reset_known_order_to_defaults(&mut self, group: ToolbarItemOrderGroup) -> bool { + self.order.reset_known_group_to_defaults(group) + } + + fn set_known_order(&mut self, group: ToolbarItemOrderGroup, ids: &[ToolbarItemId]) -> bool { + self.order.set_known_group_order(group, ids) + } } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct ResolvedToolbarItems { pub hidden: BTreeSet, pub unknown_hidden: Vec, + pub order: ResolvedToolbarOrder, } impl ResolvedToolbarItems { @@ -103,6 +196,288 @@ impl ResolvedToolbarItems { } } +#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct ToolbarItemOrderConfig { + #[serde(default)] + pub top_tools: Vec, + #[serde(default)] + pub top_controls: Vec, + #[serde(default)] + pub side_sections: Vec, + #[serde(default)] + pub actions: Vec, + #[serde(default)] + pub pages: Vec, + #[serde(default)] + pub boards: Vec, + #[serde(default)] + pub presets: Vec, + #[serde(default)] + pub tool_options: Vec, + #[serde(default)] + pub sessions: Vec, +} + +impl ToolbarItemOrderConfig { + pub fn resolved(&self) -> ResolvedToolbarOrder { + ResolvedToolbarOrder { + top_tools: resolve_order_group(ToolbarItemOrderGroup::TopTools, &self.top_tools), + top_controls: resolve_order_group( + ToolbarItemOrderGroup::TopControls, + &self.top_controls, + ), + side_sections: resolve_order_group( + ToolbarItemOrderGroup::SideSections, + &self.side_sections, + ), + actions: resolve_order_group(ToolbarItemOrderGroup::Actions, &self.actions), + pages: resolve_order_group(ToolbarItemOrderGroup::Pages, &self.pages), + boards: resolve_order_group(ToolbarItemOrderGroup::Boards, &self.boards), + presets: resolve_order_group(ToolbarItemOrderGroup::Presets, &self.presets), + tool_options: resolve_order_group( + ToolbarItemOrderGroup::ToolOptions, + &self.tool_options, + ), + sessions: resolve_order_group(ToolbarItemOrderGroup::Sessions, &self.sessions), + } + } + + fn group_mut(&mut self, group: ToolbarItemOrderGroup) -> &mut Vec { + match group { + ToolbarItemOrderGroup::TopTools => &mut self.top_tools, + ToolbarItemOrderGroup::TopControls => &mut self.top_controls, + ToolbarItemOrderGroup::SideSections => &mut self.side_sections, + ToolbarItemOrderGroup::Actions => &mut self.actions, + ToolbarItemOrderGroup::Pages => &mut self.pages, + ToolbarItemOrderGroup::Boards => &mut self.boards, + ToolbarItemOrderGroup::Presets => &mut self.presets, + ToolbarItemOrderGroup::ToolOptions => &mut self.tool_options, + ToolbarItemOrderGroup::Sessions => &mut self.sessions, + } + } + + fn group(&self, group: ToolbarItemOrderGroup) -> &[String] { + match group { + ToolbarItemOrderGroup::TopTools => &self.top_tools, + ToolbarItemOrderGroup::TopControls => &self.top_controls, + ToolbarItemOrderGroup::SideSections => &self.side_sections, + ToolbarItemOrderGroup::Actions => &self.actions, + ToolbarItemOrderGroup::Pages => &self.pages, + ToolbarItemOrderGroup::Boards => &self.boards, + ToolbarItemOrderGroup::Presets => &self.presets, + ToolbarItemOrderGroup::ToolOptions => &self.tool_options, + ToolbarItemOrderGroup::Sessions => &self.sessions, + } + } + + fn set_known_group_order( + &mut self, + group: ToolbarItemOrderGroup, + ids: &[ToolbarItemId], + ) -> bool { + let original = self.group(group).to_vec(); + let mut next: Vec = ids + .iter() + .copied() + .filter(|id| toolbar_item_id_in_order_group(*id, group)) + .map(|id| id.as_str().to_string()) + .collect(); + append_preserved_order_strings(&original, group, &mut next); + let changed = next != original; + *self.group_mut(group) = next; + changed + } + + fn reset_known_group_to_defaults(&mut self, group: ToolbarItemOrderGroup) -> bool { + let original = self.group(group).to_vec(); + let mut next = Vec::new(); + append_preserved_order_strings(&original, group, &mut next); + let changed = next != original; + *self.group_mut(group) = next; + changed + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ResolvedToolbarOrder { + top_tools: ResolvedToolbarOrderGroup, + top_controls: ResolvedToolbarOrderGroup, + side_sections: ResolvedToolbarOrderGroup, + actions: ResolvedToolbarOrderGroup, + pages: ResolvedToolbarOrderGroup, + boards: ResolvedToolbarOrderGroup, + presets: ResolvedToolbarOrderGroup, + tool_options: ResolvedToolbarOrderGroup, + sessions: ResolvedToolbarOrderGroup, +} + +impl ResolvedToolbarOrder { + pub fn ordered_ids(&self, group: ToolbarItemOrderGroup) -> &[ToolbarItemId] { + &self.group(group).known + } + + pub fn index_of(&self, group: ToolbarItemOrderGroup, id: ToolbarItemId) -> Option { + self.ordered_ids(group) + .iter() + .position(|candidate| *candidate == id) + } + + fn group(&self, group: ToolbarItemOrderGroup) -> &ResolvedToolbarOrderGroup { + match group { + ToolbarItemOrderGroup::TopTools => &self.top_tools, + ToolbarItemOrderGroup::TopControls => &self.top_controls, + ToolbarItemOrderGroup::SideSections => &self.side_sections, + ToolbarItemOrderGroup::Actions => &self.actions, + ToolbarItemOrderGroup::Pages => &self.pages, + ToolbarItemOrderGroup::Boards => &self.boards, + ToolbarItemOrderGroup::Presets => &self.presets, + ToolbarItemOrderGroup::ToolOptions => &self.tool_options, + ToolbarItemOrderGroup::Sessions => &self.sessions, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct ResolvedToolbarOrderGroup { + known: Vec, + unknown: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ToolbarItemOrderGroup { + TopTools, + TopControls, + SideSections, + Actions, + Pages, + Boards, + Presets, + ToolOptions, + Sessions, +} + +pub fn toolbar_item_order_group( + definition: &ToolbarItemDefinition, +) -> Option { + match (definition.surface, definition.category, definition.group) { + (ToolbarItemSurface::Top, ToolbarItemCategory::Tool, _) => { + Some(ToolbarItemOrderGroup::TopTools) + } + (ToolbarItemSurface::Top, ToolbarItemCategory::Utility, _) + if top_control_orderable(definition.id) => + { + Some(ToolbarItemOrderGroup::TopControls) + } + (_, ToolbarItemCategory::Group, Some(group)) if side_section_orderable(group) => { + Some(ToolbarItemOrderGroup::SideSections) + } + (_, ToolbarItemCategory::Action, _) => Some(ToolbarItemOrderGroup::Actions), + (_, ToolbarItemCategory::Page, _) => Some(ToolbarItemOrderGroup::Pages), + (_, ToolbarItemCategory::Board, _) => Some(ToolbarItemOrderGroup::Boards), + (_, ToolbarItemCategory::ToolOption, _) => Some(ToolbarItemOrderGroup::ToolOptions), + (_, ToolbarItemCategory::Session, _) => Some(ToolbarItemOrderGroup::Sessions), + (_, _, Some(ToolbarGroupId::Presets)) => Some(ToolbarItemOrderGroup::Presets), + _ => None, + } +} + +fn top_control_orderable(id: ToolbarItemId) -> bool { + DEFAULT_TOP_CONTROLS_ORDER.contains(&id) +} + +fn side_section_orderable(group: ToolbarGroupId) -> bool { + matches!( + group, + ToolbarGroupId::Colors + | ToolbarGroupId::Thickness + | ToolbarGroupId::ArrowLabels + | ToolbarGroupId::StepMarkers + | ToolbarGroupId::MarkerOpacity + | ToolbarGroupId::TextSize + | ToolbarGroupId::Actions + | ToolbarGroupId::Pages + | ToolbarGroupId::Boards + | ToolbarGroupId::Presets + | ToolbarGroupId::StepUndo + | ToolbarGroupId::Session + | ToolbarGroupId::Settings + ) +} + +pub fn toolbar_item_id_in_order_group(id: ToolbarItemId, group: ToolbarItemOrderGroup) -> bool { + toolbar_item_definitions() + .iter() + .find(|definition| definition.id == id) + .and_then(toolbar_item_order_group) + == Some(group) +} + +fn resolve_order_group(group: ToolbarItemOrderGroup, raw: &[String]) -> ResolvedToolbarOrderGroup { + let defaults = default_order_for_group(group); + if raw.is_empty() { + return ResolvedToolbarOrderGroup { + known: defaults, + unknown: Vec::new(), + }; + } + + let mut known = Vec::with_capacity(defaults.len()); + let mut seen = BTreeSet::new(); + let mut unknown = Vec::new(); + for value in raw { + match value.parse::() { + Ok(id) if toolbar_item_id_in_order_group(id, group) => { + if seen.insert(id) { + known.push(id); + } + } + _ => unknown.push(value.clone()), + } + } + for id in defaults { + if seen.insert(id) { + known.push(id); + } + } + + ResolvedToolbarOrderGroup { known, unknown } +} + +fn default_order_for_group(group: ToolbarItemOrderGroup) -> Vec { + let default_visual_order = match group { + ToolbarItemOrderGroup::TopTools => Some(DEFAULT_TOP_TOOLS_ORDER), + ToolbarItemOrderGroup::TopControls => Some(DEFAULT_TOP_CONTROLS_ORDER), + ToolbarItemOrderGroup::SideSections => Some(DEFAULT_SIDE_SECTIONS_ORDER), + _ => None, + }; + if let Some(order) = default_visual_order { + return order.to_vec(); + } + + toolbar_item_definitions() + .iter() + .filter(|definition| toolbar_item_order_group(definition) == Some(group)) + .map(|definition| definition.id) + .collect() +} + +fn append_preserved_order_strings( + original: &[String], + group: ToolbarItemOrderGroup, + next: &mut Vec, +) { + for raw in original { + if raw + .parse::() + .is_ok_and(|id| toolbar_item_id_in_order_group(id, group)) + { + continue; + } + next.push(raw.clone()); + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ToolbarItemId(&'static str); @@ -149,14 +524,14 @@ pub struct ToolbarItemDefinition { impl ToolbarItemDefinition { const fn new( - id: &'static str, + id: ToolbarItemId, label: &'static str, surface: ToolbarItemSurface, category: ToolbarItemCategory, group: Option, ) -> Self { Self { - id: ToolbarItemId::from_known(id), + id, label, surface, category, @@ -228,24 +603,24 @@ impl ToolbarGroupId { } pub const fn toolbar_item_id(self) -> ToolbarItemId { - ToolbarItemId::from_known(match self { - Self::Colors => "side.group.colors", - Self::Thickness => "side.group.thickness", - Self::EraserMode => "side.group.eraser-mode", - Self::PolygonSides => "side.group.polygon-sides", - Self::ArrowLabels => "side.group.arrow-labels", - Self::StepMarkers => "side.group.step-markers", - Self::StepUndo => "side.group.step-undo", - Self::MarkerOpacity => "side.group.marker-opacity", - Self::TextSize => "side.group.text-size", - Self::Font => "side.group.font", - Self::Actions => "side.group.actions", - Self::Pages => "side.group.pages", - Self::Boards => "side.group.boards", - Self::Presets => "side.group.presets", - Self::Settings => "side.group.settings", - Self::Session => "side.group.session", - }) + match self { + Self::Colors => ids::SIDE_GROUP_COLORS, + Self::Thickness => ids::SIDE_GROUP_THICKNESS, + Self::EraserMode => ids::SIDE_GROUP_ERASER_MODE, + Self::PolygonSides => ids::SIDE_GROUP_POLYGON_SIDES, + Self::ArrowLabels => ids::SIDE_GROUP_ARROW_LABELS, + Self::StepMarkers => ids::SIDE_GROUP_STEP_MARKERS, + Self::StepUndo => ids::SIDE_GROUP_STEP_UNDO, + Self::MarkerOpacity => ids::SIDE_GROUP_MARKER_OPACITY, + Self::TextSize => ids::SIDE_GROUP_TEXT_SIZE, + Self::Font => ids::SIDE_GROUP_FONT, + Self::Actions => ids::SIDE_GROUP_ACTIONS, + Self::Pages => ids::SIDE_GROUP_PAGES, + Self::Boards => ids::SIDE_GROUP_BOARDS, + Self::Presets => ids::SIDE_GROUP_PRESETS, + Self::Settings => ids::SIDE_GROUP_SETTINGS, + Self::Session => ids::SIDE_GROUP_SESSION, + } } } @@ -289,554 +664,578 @@ pub fn toolbar_item_definitions() -> &'static [ToolbarItemDefinition] { } const TOOLBAR_ITEM_DEFINITIONS: &[ToolbarItemDefinition] = &[ - item("top.chrome.drag", "Move top toolbar", Top, Chrome, None), - item("top.chrome.pin", "Pin top toolbar", Top, Chrome, None), - item("top.chrome.close", "Close top toolbar", Top, Chrome, None), - item("top.tool.select", "Select", Top, Tool, None), - item("top.tool.pen", "Pen", Top, Tool, None), - item("top.tool.marker", "Marker", Top, Tool, None), - item("top.tool.step-marker", "Step marker", Top, Tool, None), - item("top.tool.eraser", "Eraser", Top, Tool, None), - item("top.tool.line", "Line", Top, Tool, None), - item("top.tool.rect", "Rectangle", Top, Tool, None), - item("top.tool.ellipse", "Ellipse", Top, Tool, None), - item("top.tool.arrow", "Arrow", Top, Tool, None), - item("top.tool.blur", "Blur", Top, Tool, None), - item("top.tool.triangle", "Triangle", Top, Tool, None), - item("top.tool.parallelogram", "Parallelogram", Top, Tool, None), - item("top.tool.rhombus", "Rhombus", Top, Tool, None), - item( - "top.tool.regular-polygon", + item(ids::TOP_CHROME_DRAG, "Move top toolbar", Top, Chrome, None), + item(ids::TOP_CHROME_PIN, "Pin top toolbar", Top, Chrome, None), + item( + ids::TOP_CHROME_CLOSE, + "Close top toolbar", + Top, + Chrome, + None, + ), + item(ids::TOP_TOOL_SELECT, "Select", Top, Tool, None), + item(ids::TOP_TOOL_PEN, "Pen", Top, Tool, None), + item(ids::TOP_TOOL_MARKER, "Marker", Top, Tool, None), + item(ids::TOP_TOOL_STEP_MARKER, "Step marker", Top, Tool, None), + item(ids::TOP_TOOL_ERASER, "Eraser", Top, Tool, None), + item(ids::TOP_TOOL_LINE, "Line", Top, Tool, None), + item(ids::TOP_TOOL_RECT, "Rectangle", Top, Tool, None), + item(ids::TOP_TOOL_ELLIPSE, "Ellipse", Top, Tool, None), + item(ids::TOP_TOOL_ARROW, "Arrow", Top, Tool, None), + item(ids::TOP_TOOL_BLUR, "Blur", Top, Tool, None), + item(ids::TOP_TOOL_TRIANGLE, "Triangle", Top, Tool, None), + item( + ids::TOP_TOOL_PARALLELOGRAM, + "Parallelogram", + Top, + Tool, + None, + ), + item(ids::TOP_TOOL_RHOMBUS, "Rhombus", Top, Tool, None), + item( + ids::TOP_TOOL_REGULAR_POLYGON, "Regular polygon", Top, Tool, None, ), item( - "top.tool.freeform-polygon", + ids::TOP_TOOL_FREEFORM_POLYGON, "Freeform polygon", Top, Tool, None, ), item( - "top.utility.shape-picker", + ids::TOP_UTILITY_SHAPE_PICKER, "Shape picker", Top, Utility, None, ), - item("top.utility.fill", "Fill", Top, Utility, None), - item("top.utility.text", "Text", Top, Utility, None), - item("top.utility.sticky-note", "Sticky note", Top, Utility, None), + item(ids::TOP_UTILITY_FILL, "Fill", Top, Utility, None), + item(ids::TOP_UTILITY_TEXT, "Text", Top, Utility, None), + item( + ids::TOP_UTILITY_STICKY_NOTE, + "Sticky note", + Top, + Utility, + None, + ), item( - "top.utility.clear-canvas", + ids::TOP_UTILITY_CLEAR_CANVAS, "Clear canvas", Top, Utility, None, ), - item("top.utility.screenshot", "Screenshot", Top, Utility, None), - item("top.utility.highlight", "Highlight", Top, Utility, None), item( - "top.utility.highlight-ring", + ids::TOP_UTILITY_SCREENSHOT, + "Screenshot", + Top, + Utility, + None, + ), + item(ids::TOP_UTILITY_HIGHLIGHT, "Highlight", Top, Utility, None), + item( + ids::TOP_UTILITY_HIGHLIGHT_RING, "Highlight ring", Top, Utility, None, ), item( - "top.utility.icon-mode-icons", + ids::TOP_UTILITY_ICON_MODE_ICONS, "Use icons", Top, Utility, None, ), item( - "top.utility.icon-mode-text", + ids::TOP_UTILITY_ICON_MODE_TEXT, "Use text labels", Top, Utility, None, ), item( - "side.group.colors", + ids::SIDE_GROUP_COLORS, "Colors", Side, Group, Some(ToolbarGroupId::Colors), ), item( - "side.group.thickness", + ids::SIDE_GROUP_THICKNESS, "Thickness", Side, Group, Some(ToolbarGroupId::Thickness), ), item( - "side.group.eraser-mode", + ids::SIDE_GROUP_ERASER_MODE, "Eraser mode", Side, Group, Some(ToolbarGroupId::EraserMode), ), item( - "side.group.polygon-sides", + ids::SIDE_GROUP_POLYGON_SIDES, "Polygon sides", Side, Group, Some(ToolbarGroupId::PolygonSides), ), item( - "side.group.arrow-labels", + ids::SIDE_GROUP_ARROW_LABELS, "Arrow labels", Side, Group, Some(ToolbarGroupId::ArrowLabels), ), item( - "side.group.step-markers", + ids::SIDE_GROUP_STEP_MARKERS, "Step markers", Side, Group, Some(ToolbarGroupId::StepMarkers), ), item( - "side.group.step-undo", + ids::SIDE_GROUP_STEP_UNDO, "Step Undo/Redo", Side, Group, Some(ToolbarGroupId::StepUndo), ), item( - "side.group.marker-opacity", + ids::SIDE_GROUP_MARKER_OPACITY, "Marker opacity", Side, Group, Some(ToolbarGroupId::MarkerOpacity), ), item( - "side.group.text-size", + ids::SIDE_GROUP_TEXT_SIZE, "Text size", Side, Group, Some(ToolbarGroupId::TextSize), ), item( - "side.group.font", + ids::SIDE_GROUP_FONT, "Font", Side, Group, Some(ToolbarGroupId::Font), ), item( - "side.group.actions", + ids::SIDE_GROUP_ACTIONS, "Actions", Side, Group, Some(ToolbarGroupId::Actions), ), item( - "side.group.pages", + ids::SIDE_GROUP_PAGES, "Pages", Side, Group, Some(ToolbarGroupId::Pages), ), item( - "side.group.boards", + ids::SIDE_GROUP_BOARDS, "Boards", Side, Group, Some(ToolbarGroupId::Boards), ), item( - "side.group.presets", + ids::SIDE_GROUP_PRESETS, "Presets", Side, Group, Some(ToolbarGroupId::Presets), ), item( - "side.group.settings", + ids::SIDE_GROUP_SETTINGS, "Settings", Side, Group, Some(ToolbarGroupId::Settings), ), item( - "side.group.session", + ids::SIDE_GROUP_SESSION, "Session", Side, Group, Some(ToolbarGroupId::Session), ), item( - "side.actions.undo", + ids::SIDE_ACTIONS_UNDO, "Undo", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.redo", + ids::SIDE_ACTIONS_REDO, "Redo", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.clear-canvas", + ids::SIDE_ACTIONS_CLEAR_CANVAS, "Clear canvas", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.zoom-in", + ids::SIDE_ACTIONS_ZOOM_IN, "Zoom in", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.zoom-out", + ids::SIDE_ACTIONS_ZOOM_OUT, "Zoom out", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.reset-zoom", + ids::SIDE_ACTIONS_RESET_ZOOM, "Reset zoom", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.toggle-zoom-lock", + ids::SIDE_ACTIONS_TOGGLE_ZOOM_LOCK, "Toggle zoom lock", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.undo-all", + ids::SIDE_ACTIONS_UNDO_ALL, "Undo all", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.redo-all", + ids::SIDE_ACTIONS_REDO_ALL, "Redo all", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.undo-all-delayed", + ids::SIDE_ACTIONS_UNDO_ALL_DELAYED, "Delayed undo all", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.redo-all-delayed", + ids::SIDE_ACTIONS_REDO_ALL_DELAYED, "Delayed redo all", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.actions.freeze", + ids::SIDE_ACTIONS_FREEZE, "Freeze", Side, Action, Some(ToolbarGroupId::Actions), ), item( - "side.pages.previous", + ids::SIDE_PAGES_PREVIOUS, "Previous page", Side, Page, Some(ToolbarGroupId::Pages), ), item( - "side.pages.next", + ids::SIDE_PAGES_NEXT, "Next page", Side, Page, Some(ToolbarGroupId::Pages), ), item( - "side.pages.new", + ids::SIDE_PAGES_NEW, "New page", Side, Page, Some(ToolbarGroupId::Pages), ), item( - "side.pages.duplicate", + ids::SIDE_PAGES_DUPLICATE, "Duplicate page", Side, Page, Some(ToolbarGroupId::Pages), ), item( - "side.pages.delete", + ids::SIDE_PAGES_DELETE, "Delete page", Side, Page, Some(ToolbarGroupId::Pages), ), item( - "side.boards.previous", + ids::SIDE_BOARDS_PREVIOUS, "Previous board", Side, Board, Some(ToolbarGroupId::Boards), ), item( - "side.boards.next", + ids::SIDE_BOARDS_NEXT, "Next board", Side, Board, Some(ToolbarGroupId::Boards), ), item( - "side.boards.new", + ids::SIDE_BOARDS_NEW, "New board", Side, Board, Some(ToolbarGroupId::Boards), ), item( - "side.boards.duplicate", + ids::SIDE_BOARDS_DUPLICATE, "Duplicate board", Side, Board, Some(ToolbarGroupId::Boards), ), item( - "side.boards.delete", + ids::SIDE_BOARDS_DELETE, "Delete board", Side, Board, Some(ToolbarGroupId::Boards), ), item( - "side.boards.rename", + ids::SIDE_BOARDS_RENAME, "Rename board", Side, Board, Some(ToolbarGroupId::Boards), ), item( - "side.settings.context-aware-ui", + ids::SIDE_SETTINGS_CONTEXT_AWARE_UI, "Context UI", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.text-controls", + ids::SIDE_SETTINGS_TEXT_CONTROLS, "Text controls", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.status-bar", + ids::SIDE_SETTINGS_STATUS_BAR, "Status bar", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.status-board-badge", + ids::SIDE_SETTINGS_STATUS_BOARD_BADGE, "Status board badge", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.status-page-badge", + ids::SIDE_SETTINGS_STATUS_PAGE_BADGE, "Status page badge", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.floating-badge-always", + ids::SIDE_SETTINGS_FLOATING_BADGE_ALWAYS, "Floating board/page badge", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.preset-toasts", + ids::SIDE_SETTINGS_PRESET_TOASTS, "Preset toasts", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.presets", + ids::SIDE_SETTINGS_PRESETS, "Presets toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.actions", + ids::SIDE_SETTINGS_ACTIONS, "Actions toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.zoom-actions", + ids::SIDE_SETTINGS_ZOOM_ACTIONS, "Zoom actions toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.advanced-actions", + ids::SIDE_SETTINGS_ADVANCED_ACTIONS, "Advanced actions toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.boards", + ids::SIDE_SETTINGS_BOARDS, "Boards toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.pages", + ids::SIDE_SETTINGS_PAGES, "Pages toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.step-controls", + ids::SIDE_SETTINGS_STEP_CONTROLS, "Step controls toggle", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.configurator", + ids::SIDE_SETTINGS_CONFIGURATOR, "Open configurator", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.settings.config-file", + ids::SIDE_SETTINGS_CONFIG_FILE, "Open config file", Side, Setting, Some(ToolbarGroupId::Settings), ), item( - "side.session.open", + ids::SIDE_SESSION_OPEN, "Open session", Side, Session, Some(ToolbarGroupId::Session), ), item( - "side.session.save-as", + ids::SIDE_SESSION_SAVE_AS, "Save session as", Side, Session, Some(ToolbarGroupId::Session), ), item( - "side.session.info", + ids::SIDE_SESSION_INFO, "Session info", Side, Session, Some(ToolbarGroupId::Session), ), item( - "side.session.clear", + ids::SIDE_SESSION_CLEAR, "Clear session", Side, Session, Some(ToolbarGroupId::Session), ), item( - "side.session.manager", + ids::SIDE_SESSION_MANAGER, "Session manager", Side, Session, Some(ToolbarGroupId::Session), ), item( - "side.tool-options.color", + ids::SIDE_TOOL_OPTIONS_COLOR, "Color", Side, ToolOption, Some(ToolbarGroupId::Colors), ), item( - "side.tool-options.thickness", + ids::SIDE_TOOL_OPTIONS_THICKNESS, "Thickness", Side, ToolOption, Some(ToolbarGroupId::Thickness), ), item( - "side.tool-options.marker-opacity", + ids::SIDE_TOOL_OPTIONS_MARKER_OPACITY, "Marker opacity", Side, ToolOption, Some(ToolbarGroupId::MarkerOpacity), ), item( - "side.tool-options.eraser-mode", + ids::SIDE_TOOL_OPTIONS_ERASER_MODE, "Eraser mode", Side, ToolOption, Some(ToolbarGroupId::EraserMode), ), item( - "side.tool-options.font-size", + ids::SIDE_TOOL_OPTIONS_FONT_SIZE, "Font size", Side, ToolOption, Some(ToolbarGroupId::TextSize), ), item( - "side.tool-options.font-family", + ids::SIDE_TOOL_OPTIONS_FONT_FAMILY, "Font family", Side, ToolOption, Some(ToolbarGroupId::Font), ), item( - "side.tool-options.polygon-sides", + ids::SIDE_TOOL_OPTIONS_POLYGON_SIDES, "Polygon sides", Side, ToolOption, Some(ToolbarGroupId::PolygonSides), ), item( - "side.tool-options.arrow-labels", + ids::SIDE_TOOL_OPTIONS_ARROW_LABELS, "Arrow labels", Side, ToolOption, Some(ToolbarGroupId::ArrowLabels), ), item( - "side.tool-options.step-marker-reset", + ids::SIDE_TOOL_OPTIONS_STEP_MARKER_RESET, "Reset step marker", Side, ToolOption, @@ -845,7 +1244,7 @@ const TOOLBAR_ITEM_DEFINITIONS: &[ToolbarItemDefinition] = &[ ]; const fn item( - id: &'static str, + id: ToolbarItemId, label: &'static str, surface: ToolbarItemSurface, category: ToolbarItemCategory, @@ -867,14 +1266,15 @@ mod tests { fn known_hidden_ids_resolve_and_unknown_ids_round_trip() { let config = ToolbarItemsConfig { hidden: vec![ - "side.actions.undo-all".to_string(), + ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(), "future.toolbar.item".to_string(), ], + order: ToolbarItemOrderConfig::default(), }; let resolved = config.resolved(); - assert!(resolved.is_hidden("side.actions.undo-all".parse().expect("known id"))); + assert!(resolved.is_hidden(ids::SIDE_ACTIONS_UNDO_ALL)); assert_eq!(resolved.unknown_hidden, vec!["future.toolbar.item"]); } @@ -882,7 +1282,7 @@ mod tests { fn default_hidden_items_hide_screenshot_tool() { let resolved = ToolbarItemsConfig::default().resolved(); - assert!(resolved.is_hidden("top.utility.screenshot".parse().expect("known id"))); + assert!(resolved.is_hidden(ids::TOP_UTILITY_SCREENSHOT)); } #[test] @@ -890,21 +1290,22 @@ mod tests { let mut config = ToolbarItemsConfig { hidden: vec![ "future.toolbar.item".to_string(), - "side.actions.undo-all".to_string(), - "side.actions.undo-all".to_string(), - "side.pages.duplicate".to_string(), + ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(), + ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(), + ids::SIDE_PAGES_DUPLICATE.as_str().to_string(), ], + order: ToolbarItemOrderConfig::default(), }; - config.set_hidden("side.actions.undo-all".parse().expect("known id"), false); - config.set_hidden("top.tool.pen".parse().expect("known id"), true); + config.set_hidden(ids::SIDE_ACTIONS_UNDO_ALL, false); + config.set_hidden(ids::TOP_TOOL_PEN, true); assert_eq!( config.hidden, vec![ - "future.toolbar.item", - "side.pages.duplicate", - "top.tool.pen" + "future.toolbar.item".to_string(), + ids::SIDE_PAGES_DUPLICATE.as_str().to_string(), + ids::TOP_TOOL_PEN.as_str().to_string() ] ); } @@ -914,18 +1315,119 @@ mod tests { let mut config = ToolbarItemsConfig { hidden: vec![ "future.toolbar.item".to_string(), - "side.actions.undo-all".to_string(), + ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(), ], + order: ToolbarItemOrderConfig::default(), }; assert!(config.reset_known_hidden_to_defaults()); assert_eq!( config.hidden, - vec!["top.utility.screenshot", "future.toolbar.item"] + vec![ + ids::TOP_UTILITY_SCREENSHOT.as_str().to_string(), + "future.toolbar.item".to_string() + ] ); assert!(!config.reset_known_hidden_to_defaults()); } + #[test] + fn default_order_matches_visual_toolbar_defaults() { + let resolved = ToolbarItemsConfig::default().resolved(); + + assert_eq!( + resolved.order.ordered_ids(ToolbarItemOrderGroup::TopTools), + DEFAULT_TOP_TOOLS_ORDER + ); + assert_eq!( + resolved + .order + .ordered_ids(ToolbarItemOrderGroup::TopControls), + DEFAULT_TOP_CONTROLS_ORDER + ); + assert_eq!( + resolved + .order + .ordered_ids(ToolbarItemOrderGroup::SideSections), + DEFAULT_SIDE_SECTIONS_ORDER + ); + } + + #[test] + fn item_order_moves_known_ids_and_preserves_unknown_ids() { + let mut config = ToolbarItemsConfig { + hidden: Vec::new(), + order: ToolbarItemOrderConfig { + top_tools: vec![ + "future.toolbar.item".to_string(), + ids::TOP_TOOL_PEN.as_str().to_string(), + ids::TOP_TOOL_SELECT.as_str().to_string(), + ], + ..ToolbarItemOrderConfig::default() + }, + }; + + assert!(config.move_item_by(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN, 1,)); + + assert_eq!( + config.order.top_tools.last(), + Some(&"future.toolbar.item".to_string()) + ); + assert_eq!( + config + .resolved() + .order + .ordered_ids(ToolbarItemOrderGroup::TopTools)[1], + ids::TOP_TOOL_PEN + ); + } + + #[test] + fn top_control_order_excludes_visibility_only_utilities() { + let config = ToolbarItemsConfig { + hidden: Vec::new(), + order: ToolbarItemOrderConfig { + top_controls: vec![ + ids::TOP_UTILITY_SHAPE_PICKER.as_str().to_string(), + ids::TOP_UTILITY_TEXT.as_str().to_string(), + ids::TOP_UTILITY_FILL.as_str().to_string(), + ], + ..ToolbarItemOrderConfig::default() + }, + }; + + let resolved = config.resolved(); + let ordered = resolved + .order + .ordered_ids(ToolbarItemOrderGroup::TopControls); + assert_eq!(ordered[0], ids::TOP_UTILITY_TEXT); + assert!(!ordered.contains(&ids::TOP_UTILITY_SHAPE_PICKER)); + assert!(!ordered.contains(&ids::TOP_UTILITY_FILL)); + } + + #[test] + fn side_section_order_uses_runtime_representable_blocks() { + let config = ToolbarItemsConfig { + hidden: Vec::new(), + order: ToolbarItemOrderConfig { + side_sections: vec![ + ids::SIDE_GROUP_FONT.as_str().to_string(), + ids::SIDE_GROUP_THICKNESS.as_str().to_string(), + ids::SIDE_GROUP_POLYGON_SIDES.as_str().to_string(), + ], + ..ToolbarItemOrderConfig::default() + }, + }; + + let resolved = config.resolved(); + let ordered = resolved + .order + .ordered_ids(ToolbarItemOrderGroup::SideSections); + assert_eq!(ordered[0], ids::SIDE_GROUP_THICKNESS); + assert!(!ordered.contains(&ids::SIDE_GROUP_FONT)); + assert!(!ordered.contains(&ids::SIDE_GROUP_POLYGON_SIDES)); + } + #[test] fn toolbar_group_ids_include_step_markers_and_step_undo() { assert_eq!( diff --git a/src/config/types/toolbar/mod.rs b/src/config/types/toolbar/mod.rs index f17b33ac..e4a6a806 100644 --- a/src/config/types/toolbar/mod.rs +++ b/src/config/types/toolbar/mod.rs @@ -1,4 +1,5 @@ mod config; +pub mod ids; mod items; mod mode; mod overrides; @@ -6,7 +7,8 @@ mod overrides; pub use config::ToolbarConfig; pub use items::{ ResolvedToolbarItems, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, - ToolbarItemId, ToolbarItemSurface, ToolbarItemsConfig, toolbar_item_definitions, + ToolbarItemId, ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, + ToolbarItemsConfig, toolbar_item_definitions, toolbar_item_order_group, }; pub use mode::{ToolbarLayoutMode, ToolbarSectionDefaults}; pub use overrides::{ToolbarModeOverride, ToolbarModeOverrides}; diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index d0303ee1..7ff99b16 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -143,6 +143,7 @@ impl InputState { toolbar_mode_overrides: crate::config::ToolbarModeOverrides::default(), toolbar_items: crate::config::ToolbarItemsConfig::default(), resolved_toolbar_items: crate::config::ToolbarItemsConfig::default().resolved(), + toolbar_customize_drag: None, toolbar_shapes_expanded: false, toolbar_drawer_open: false, toolbar_drawer_tab: ToolbarDrawerTab::View, diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index 22e5eec1..be96ea00 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -21,7 +21,8 @@ use super::super::types::{ }; use crate::config::{ Action, BoardsConfig, KeyBinding, PresenterModeConfig, RadialMenuMouseBinding, - ResolvedToolbarItems, ToolPresetConfig, ToolbarItemsConfig, + ResolvedToolbarItems, ToolPresetConfig, ToolbarItemId, ToolbarItemOrderGroup, + ToolbarItemsConfig, }; use crate::draw::frame::ShapeSnapshot; use crate::draw::{Color, DirtyTracker, EraserKind, FontDescriptor, Shape, ShapeId}; @@ -204,6 +205,8 @@ pub struct InputState { pub toolbar_items: ToolbarItemsConfig, /// Resolved known item-level toolbar visibility config. pub resolved_toolbar_items: ResolvedToolbarItems, + /// Active toolbar customization reorder drag source. + pub toolbar_customize_drag: Option<(ToolbarItemOrderGroup, ToolbarItemId)>, /// Whether the simple-mode shape picker is expanded pub toolbar_shapes_expanded: bool, /// Whether the toolbar drawer is open diff --git a/src/input/state/core/tool_controls/toolbar.rs b/src/input/state/core/tool_controls/toolbar.rs index aede9ba1..a8d5edf5 100644 --- a/src/input/state/core/tool_controls/toolbar.rs +++ b/src/input/state/core/tool_controls/toolbar.rs @@ -1,5 +1,5 @@ use super::super::base::InputState; -use crate::config::{Action, ToolbarItemId}; +use crate::config::{Action, ToolbarItemId, ToolbarItemOrderGroup}; impl InputState { /// Sets toolbar visibility flag (controls both top and side). Returns true if toggled. @@ -111,6 +111,72 @@ impl InputState { true } + pub fn move_toolbar_item( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + delta: isize, + ) -> bool { + if !self.toolbar_items.move_item_by(group, id, delta) { + return false; + } + + self.resolved_toolbar_items = self.toolbar_items.resolved(); + self.needs_redraw = true; + true + } + + pub fn start_toolbar_item_drag( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + ) -> bool { + if self.toolbar_customize_drag == Some((group, id)) { + return false; + } + + self.toolbar_customize_drag = Some((group, id)); + true + } + + pub fn drag_toolbar_item_over( + &mut self, + group: ToolbarItemOrderGroup, + target_index: usize, + ) -> bool { + let Some((source_group, id)) = self.toolbar_customize_drag else { + return false; + }; + if source_group != group { + return false; + } + + if !self + .toolbar_items + .move_item_to_index(group, id, target_index) + { + return false; + } + + self.resolved_toolbar_items = self.toolbar_items.resolved(); + self.needs_redraw = true; + true + } + + pub fn clear_toolbar_item_drag(&mut self) { + self.toolbar_customize_drag = None; + } + + pub fn reset_toolbar_item_order(&mut self, group: ToolbarItemOrderGroup) -> bool { + if !self.toolbar_items.reset_known_order_to_defaults(group) { + return false; + } + + self.resolved_toolbar_items = self.toolbar_items.resolved(); + self.needs_redraw = true; + true + } + fn apply_toolbar_mode_overrides(&mut self, mode: crate::config::ToolbarLayoutMode) { let overrides = self.toolbar_mode_overrides.for_mode(mode); if let Some(value) = overrides.show_actions_section { diff --git a/src/ui/toolbar/apply/layout.rs b/src/ui/toolbar/apply/layout.rs index 1eb1f3c8..3f515bb7 100644 --- a/src/ui/toolbar/apply/layout.rs +++ b/src/ui/toolbar/apply/layout.rs @@ -1,4 +1,4 @@ -use crate::config::{ToolbarItemId, ToolbarLayoutMode}; +use crate::config::{ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode}; use crate::input::{InputState, ToolbarDrawerTab}; use crate::ui::toolbar::{ToolbarItemCustomizeGroup, ToolbarSideSection}; @@ -333,6 +333,35 @@ impl InputState { self.set_toolbar_item_hidden(id, hidden) } + pub(super) fn apply_toolbar_move_item( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + delta: isize, + ) -> bool { + self.move_toolbar_item(group, id, delta) + } + + pub(super) fn apply_toolbar_start_item_drag( + &mut self, + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + ) -> bool { + self.start_toolbar_item_drag(group, id) + } + + pub(super) fn apply_toolbar_drag_item_over( + &mut self, + group: ToolbarItemOrderGroup, + target_index: usize, + ) -> bool { + self.drag_toolbar_item_over(group, target_index) + } + + pub(super) fn apply_toolbar_reset_item_order(&mut self, group: ToolbarItemOrderGroup) -> bool { + self.reset_toolbar_item_order(group) + } + pub(super) fn apply_toolbar_reset_item_hidden_overrides(&mut self) -> bool { self.reset_toolbar_item_hidden_overrides() } diff --git a/src/ui/toolbar/apply/mod.rs b/src/ui/toolbar/apply/mod.rs index 8358a590..72827053 100644 --- a/src/ui/toolbar/apply/mod.rs +++ b/src/ui/toolbar/apply/mod.rs @@ -139,6 +139,19 @@ impl InputState { ToolbarEvent::SetToolbarItemHidden(id, hidden) => { self.apply_toolbar_set_item_hidden(id, hidden) } + ToolbarEvent::MoveToolbarItem { group, id, delta } => { + self.apply_toolbar_move_item(group, id, delta) + } + ToolbarEvent::StartToolbarItemDrag { group, id } => { + self.apply_toolbar_start_item_drag(group, id) + } + ToolbarEvent::DragToolbarItemOver { + group, + target_index, + } => self.apply_toolbar_drag_item_over(group, target_index), + ToolbarEvent::ResetToolbarItemOrder(group) => { + self.apply_toolbar_reset_item_order(group) + } ToolbarEvent::ResetToolbarItemHiddenOverrides => { self.apply_toolbar_reset_item_hidden_overrides() } diff --git a/src/ui/toolbar/events.rs b/src/ui/toolbar/events.rs index 61a30104..4be6896e 100644 --- a/src/ui/toolbar/events.rs +++ b/src/ui/toolbar/events.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::config::{Action, ToolbarItemId, ToolbarLayoutMode}; +use crate::config::{Action, ToolbarItemId, ToolbarItemOrderGroup, ToolbarLayoutMode}; use crate::draw::{Color, FontDescriptor}; use crate::input::{EraserMode, Tool, ToolbarDrawerTab}; @@ -212,6 +212,24 @@ pub enum ToolbarEvent { SetToolbarLayoutMode(ToolbarLayoutMode), /// Hide or show a known toolbar item override. SetToolbarItemHidden(ToolbarItemId, bool), + /// Move an orderable toolbar item by a relative row delta. + MoveToolbarItem { + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + delta: isize, + }, + /// Begin dragging an orderable toolbar item in the customization panel. + StartToolbarItemDrag { + group: ToolbarItemOrderGroup, + id: ToolbarItemId, + }, + /// Move the active dragged toolbar item over a target row. + DragToolbarItemOver { + group: ToolbarItemOrderGroup, + target_index: usize, + }, + /// Reset known order overrides for one toolbar item group. + ResetToolbarItemOrder(ToolbarItemOrderGroup), /// Clear known hidden toolbar item overrides, preserving unknown/future IDs. ResetToolbarItemHiddenOverrides, /// Show or hide the Settings drawer toolbar-item customization sub-panel. diff --git a/src/ui/toolbar/model/actions.rs b/src/ui/toolbar/model/actions.rs index 33eefc1d..458ea268 100644 --- a/src/ui/toolbar/model/actions.rs +++ b/src/ui/toolbar/model/actions.rs @@ -1,4 +1,4 @@ -use crate::config::ToolbarItemId; +use crate::config::{ToolbarItemId, toolbar_item_ids as ids}; use crate::input::ToolbarDrawerTab; use super::super::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; @@ -221,30 +221,30 @@ fn toolbar_button_visible(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> b } fn toolbar_button_item_id(event: &ToolbarEvent) -> Option { - Some(ToolbarItemId::from_known(match event { - ToolbarEvent::Undo => "side.actions.undo", - ToolbarEvent::Redo => "side.actions.redo", - ToolbarEvent::ClearCanvas => "side.actions.clear-canvas", - ToolbarEvent::ZoomIn => "side.actions.zoom-in", - ToolbarEvent::ZoomOut => "side.actions.zoom-out", - ToolbarEvent::ResetZoom => "side.actions.reset-zoom", - ToolbarEvent::ToggleZoomLock => "side.actions.toggle-zoom-lock", - ToolbarEvent::UndoAll => "side.actions.undo-all", - ToolbarEvent::RedoAll => "side.actions.redo-all", - ToolbarEvent::UndoAllDelayed => "side.actions.undo-all-delayed", - ToolbarEvent::RedoAllDelayed => "side.actions.redo-all-delayed", - ToolbarEvent::ToggleFreeze => "side.actions.freeze", - ToolbarEvent::PagePrev => "side.pages.previous", - ToolbarEvent::PageNext => "side.pages.next", - ToolbarEvent::PageNew => "side.pages.new", - ToolbarEvent::PageDuplicate => "side.pages.duplicate", - ToolbarEvent::PageDelete => "side.pages.delete", - ToolbarEvent::BoardPrev => "side.boards.previous", - ToolbarEvent::BoardNext => "side.boards.next", - ToolbarEvent::BoardNew => "side.boards.new", - ToolbarEvent::BoardDuplicate => "side.boards.duplicate", - ToolbarEvent::BoardDelete => "side.boards.delete", - ToolbarEvent::BoardRename => "side.boards.rename", + Some(match event { + ToolbarEvent::Undo => ids::SIDE_ACTIONS_UNDO, + ToolbarEvent::Redo => ids::SIDE_ACTIONS_REDO, + ToolbarEvent::ClearCanvas => ids::SIDE_ACTIONS_CLEAR_CANVAS, + ToolbarEvent::ZoomIn => ids::SIDE_ACTIONS_ZOOM_IN, + ToolbarEvent::ZoomOut => ids::SIDE_ACTIONS_ZOOM_OUT, + ToolbarEvent::ResetZoom => ids::SIDE_ACTIONS_RESET_ZOOM, + ToolbarEvent::ToggleZoomLock => ids::SIDE_ACTIONS_TOGGLE_ZOOM_LOCK, + ToolbarEvent::UndoAll => ids::SIDE_ACTIONS_UNDO_ALL, + ToolbarEvent::RedoAll => ids::SIDE_ACTIONS_REDO_ALL, + ToolbarEvent::UndoAllDelayed => ids::SIDE_ACTIONS_UNDO_ALL_DELAYED, + ToolbarEvent::RedoAllDelayed => ids::SIDE_ACTIONS_REDO_ALL_DELAYED, + ToolbarEvent::ToggleFreeze => ids::SIDE_ACTIONS_FREEZE, + ToolbarEvent::PagePrev => ids::SIDE_PAGES_PREVIOUS, + ToolbarEvent::PageNext => ids::SIDE_PAGES_NEXT, + ToolbarEvent::PageNew => ids::SIDE_PAGES_NEW, + ToolbarEvent::PageDuplicate => ids::SIDE_PAGES_DUPLICATE, + ToolbarEvent::PageDelete => ids::SIDE_PAGES_DELETE, + ToolbarEvent::BoardPrev => ids::SIDE_BOARDS_PREVIOUS, + ToolbarEvent::BoardNext => ids::SIDE_BOARDS_NEXT, + ToolbarEvent::BoardNew => ids::SIDE_BOARDS_NEW, + ToolbarEvent::BoardDuplicate => ids::SIDE_BOARDS_DUPLICATE, + ToolbarEvent::BoardDelete => ids::SIDE_BOARDS_DELETE, + ToolbarEvent::BoardRename => ids::SIDE_BOARDS_RENAME, _ => return None, - })) + }) } diff --git a/src/ui/toolbar/model/activation.rs b/src/ui/toolbar/model/activation.rs index 5b366b2d..a54a42f4 100644 --- a/src/ui/toolbar/model/activation.rs +++ b/src/ui/toolbar/model/activation.rs @@ -34,6 +34,7 @@ pub(crate) enum ToolbarControlId { CustomizeToolbarItems, BackToolbarSettings, ResetToolbarHiddenItems, + ResetToolbarItemOrder, OpenConfigurator, OpenConfigFile, } diff --git a/src/ui/toolbar/model/event_policy.rs b/src/ui/toolbar/model/event_policy.rs index 6a88a457..33926dde 100644 --- a/src/ui/toolbar/model/event_policy.rs +++ b/src/ui/toolbar/model/event_policy.rs @@ -188,6 +188,9 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { | ToolbarEvent::ToggleDelaySliders(_) | ToolbarEvent::SetToolbarLayoutMode(_) | ToolbarEvent::SetToolbarItemHidden(_, _) + | ToolbarEvent::MoveToolbarItem { .. } + | ToolbarEvent::DragToolbarItemOver { .. } + | ToolbarEvent::ResetToolbarItemOrder(_) | ToolbarEvent::ResetToolbarItemHiddenOverrides => ToolbarPersistence::Persist(Toolbar), ToolbarEvent::ToggleCustomSection(_) => ToolbarPersistence::Persist(History), ToolbarEvent::ToggleStatusBar(_) => ToolbarPersistence::Persist(Ui(StatusBar)), diff --git a/src/ui/toolbar/model/mod.rs b/src/ui/toolbar/model/mod.rs index da128e58..4c0d9f1d 100644 --- a/src/ui/toolbar/model/mod.rs +++ b/src/ui/toolbar/model/mod.rs @@ -38,19 +38,22 @@ pub(crate) use session::{ToolbarSessionButton, ToolbarSessionModel, ToolbarSessi pub(crate) use settings::{ToolbarSettingsButton, ToolbarSettingsModel, ToolbarSettingsToggle}; #[allow(unused_imports)] pub(crate) use tools::{ - SemanticToolIcon, common_shape_tools, current_shape_tool, default_drag_hint, + SemanticToolIcon, TopUtilityButton, current_shape_tool, default_drag_hint, default_polygon_tool, default_shape_tool, fill_tool_active, is_fill_tool, is_polygon_tool, - polygon_tools, semantic_icon_for_tool, shape_tools, tool_visible, toolbar_item_visible, - top_clear_canvas_visible, top_fill_visible, top_highlight_ring_visible, top_highlight_visible, - top_icon_mode_toggle_visible, top_screenshot_visible, top_shape_picker_visible, - top_sticky_note_visible, top_text_visible, top_tool_buttons, visible_tool_count, - visible_top_tool_buttons, + ordered_side_sections, polygon_tools, semantic_icon_for_tool, shape_tools, tool_visible, + toolbar_item_visible, top_clear_canvas_visible, top_fill_visible, top_highlight_ring_visible, + top_highlight_visible, top_icon_mode_toggle_visible, top_screenshot_visible, + top_shape_picker_visible, top_sticky_note_visible, top_text_visible, top_tool_buttons, + visible_shape_picker_max_row_len, visible_shape_picker_row_count, visible_shape_picker_rows, + visible_tool_count, visible_top_tool_buttons, visible_top_utility_buttons, }; #[cfg(test)] mod tests { use super::*; - use crate::config::ToolbarLayoutMode; + use crate::config::{ + ToolbarGroupId, ToolbarLayoutMode, toolbar_item_definitions, toolbar_item_ids as ids, + }; use crate::input::ToolbarDrawerTab; use crate::input::state::test_support::make_test_input_state; use crate::ui::toolbar::{ToolbarBindingHints, ToolbarEvent, ToolbarSnapshot}; @@ -216,7 +219,8 @@ mod tests { snapshot.drawer_tab = ToolbarDrawerTab::App; snapshot.show_settings_section = true; snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { - hidden: vec!["top.tool.pen".to_string()], + hidden: vec![ids::TOP_TOOL_PEN.as_str().to_string()], + order: crate::config::ToolbarItemOrderConfig::default(), } .resolved(); @@ -253,11 +257,12 @@ mod tests { model .item_overrides() .iter() - .any(|item| item.id.as_str() == "top.tool.pen" && !item.shown) + .any(|item| item.id == ids::TOP_TOOL_PEN && !item.shown) ); assert!(!model.item_overrides().iter().any(|item| { - item.id.as_str() == "side.group.settings" - || item.id.as_str().starts_with("side.settings.") + toolbar_item_definitions().iter().any(|definition| { + definition.id == item.id && definition.group == Some(ToolbarGroupId::Settings) + }) })); assert!(model.buttons().iter().any(|button| matches!( &button.event, diff --git a/src/ui/toolbar/model/session.rs b/src/ui/toolbar/model/session.rs index 5347e826..a6b9c19a 100644 --- a/src/ui/toolbar/model/session.rs +++ b/src/ui/toolbar/model/session.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::config::ToolbarItemId; +use crate::config::{ToolbarItemId, toolbar_item_ids as ids}; use crate::input::ToolbarDrawerTab; use super::super::{SessionRecentSnapshot, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; @@ -150,14 +150,14 @@ fn session_button_visible(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> b } fn session_button_item_id(event: &ToolbarEvent) -> Option { - Some(ToolbarItemId::from_known(match event { - ToolbarEvent::OpenSession => "side.session.open", - ToolbarEvent::SaveSessionAs => "side.session.save-as", - ToolbarEvent::SessionInfo => "side.session.info", - ToolbarEvent::ClearSession => "side.session.clear", - ToolbarEvent::OpenConfigurator => "side.session.manager", + Some(match event { + ToolbarEvent::OpenSession => ids::SIDE_SESSION_OPEN, + ToolbarEvent::SaveSessionAs => ids::SIDE_SESSION_SAVE_AS, + ToolbarEvent::SessionInfo => ids::SIDE_SESSION_INFO, + ToolbarEvent::ClearSession => ids::SIDE_SESSION_CLEAR, + ToolbarEvent::OpenConfigurator => ids::SIDE_SESSION_MANAGER, _ => return None, - })) + }) } #[cfg(test)] diff --git a/src/ui/toolbar/model/settings.rs b/src/ui/toolbar/model/settings.rs index b64267c8..62ef3134 100644 --- a/src/ui/toolbar/model/settings.rs +++ b/src/ui/toolbar/model/settings.rs @@ -2,8 +2,9 @@ use std::borrow::Cow; use crate::config::{ Action, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, - ToolbarItemSurface, ToolbarLayoutMode, action_label, action_short_label, - toolbar_item_definitions, + ToolbarItemOrderConfig, ToolbarItemOrderGroup, ToolbarItemSurface, ToolbarLayoutMode, + action_label, action_short_label, toolbar_item_definitions, toolbar_item_ids as ids, + toolbar_item_order_group, }; use super::super::{ToolbarEvent, ToolbarItemCustomizeGroup, ToolbarSideSection, ToolbarSnapshot}; @@ -164,10 +165,14 @@ impl ToolbarSettingsModel { }; let item_overrides: Vec<_> = if let Some(group) = snapshot.customize_items_group { - toolbar_item_definitions() + let mut definitions: Vec<_> = toolbar_item_definitions() .iter() .filter(|definition| customize_group_contains(group, definition)) - .map(|definition| ToolbarSettingsItemOverride::new(snapshot, definition)) + .collect(); + sort_customize_definitions(snapshot, group, &mut definitions); + definitions + .into_iter() + .map(|definition| ToolbarSettingsItemOverride::new(snapshot, group, definition)) .collect() } else { Vec::new() @@ -262,7 +267,7 @@ fn customize_buttons(snapshot: &ToolbarSnapshot) -> Vec { } else { ToolbarEvent::SetToolbarItemCustomizationOpen(false) }; - vec![ + let mut buttons = vec![ ToolbarSettingsButton { id: ToolbarControlId::BackToolbarSettings, label: Cow::Borrowed("Back"), @@ -277,10 +282,24 @@ fn customize_buttons(snapshot: &ToolbarSnapshot) -> Vec { icon: ToolbarIcon::Visibility, tooltip: ToolbarTooltip::text("Restore default hidden items"), }, - ] - .into_iter() - .filter(|button| reset_button_visible(snapshot, button.id)) - .collect() + ]; + if let Some(group) = snapshot + .customize_items_group + .and_then(customize_order_group) + .filter(|group| order_is_customized(snapshot, *group)) + { + buttons.push(ToolbarSettingsButton { + id: ToolbarControlId::ResetToolbarItemOrder, + label: Cow::Borrowed("Reset order"), + event: ToolbarEvent::ResetToolbarItemOrder(group), + icon: ToolbarIcon::Back, + tooltip: ToolbarTooltip::text("Restore default order for this group"), + }); + } + buttons + .into_iter() + .filter(|button| reset_button_visible(snapshot, button.id)) + .collect() } fn reset_button_visible(snapshot: &ToolbarSnapshot, id: ToolbarControlId) -> bool { @@ -353,30 +372,80 @@ fn customize_group_contains( } } +fn sort_customize_definitions( + snapshot: &ToolbarSnapshot, + group: ToolbarItemCustomizeGroup, + definitions: &mut Vec<&ToolbarItemDefinition>, +) { + let Some(order_group) = customize_order_group(group) else { + return; + }; + definitions.sort_by_key(|definition| { + if overlay_order_group_for_definition(definition) == Some(order_group) { + snapshot + .resolved_toolbar_items + .order + .index_of(order_group, definition.id) + .unwrap_or(usize::MAX) + } else { + usize::MAX + } + }); +} + +fn customize_order_group(group: ToolbarItemCustomizeGroup) -> Option { + match group { + ToolbarItemCustomizeGroup::TopTools => Some(ToolbarItemOrderGroup::TopTools), + ToolbarItemCustomizeGroup::TopControls => Some(ToolbarItemOrderGroup::TopControls), + ToolbarItemCustomizeGroup::SideSections => Some(ToolbarItemOrderGroup::SideSections), + _ => None, + } +} + +fn definition_order_group_for_customize( + group: ToolbarItemCustomizeGroup, + definition: &ToolbarItemDefinition, +) -> Option { + let order_group = customize_order_group(group)?; + (overlay_order_group_for_definition(definition) == Some(order_group)).then_some(order_group) +} + +fn overlay_order_group_for_definition( + definition: &ToolbarItemDefinition, +) -> Option { + toolbar_item_order_group(definition) +} + +fn order_is_customized(snapshot: &ToolbarSnapshot, group: ToolbarItemOrderGroup) -> bool { + let current = snapshot.resolved_toolbar_items.order.ordered_ids(group); + let default_order = ToolbarItemOrderConfig::default().resolved(); + current != default_order.ordered_ids(group) +} + fn control_visible(snapshot: &ToolbarSnapshot, id: ToolbarControlId) -> bool { control_item_id(id).is_none_or(|item| !snapshot.toolbar_item_hidden(item)) } fn control_item_id(id: ToolbarControlId) -> Option { - Some(ToolbarItemId::from_known(match id { - ToolbarControlId::SettingsContextAwareUi => "side.settings.context-aware-ui", - ToolbarControlId::SettingsTextControls => "side.settings.text-controls", - ToolbarControlId::SettingsStatusBar => "side.settings.status-bar", - ToolbarControlId::SettingsStatusBoardBadge => "side.settings.status-board-badge", - ToolbarControlId::SettingsStatusPageBadge => "side.settings.status-page-badge", - ToolbarControlId::SettingsFloatingBadgeAlways => "side.settings.floating-badge-always", - ToolbarControlId::SettingsPresetToasts => "side.settings.preset-toasts", - ToolbarControlId::SettingsPresets => "side.settings.presets", - ToolbarControlId::SettingsActions => "side.settings.actions", - ToolbarControlId::SettingsZoomActions => "side.settings.zoom-actions", - ToolbarControlId::SettingsAdvancedActions => "side.settings.advanced-actions", - ToolbarControlId::SettingsBoards => "side.settings.boards", - ToolbarControlId::SettingsPages => "side.settings.pages", - ToolbarControlId::SettingsStepControls => "side.settings.step-controls", - ToolbarControlId::OpenConfigurator => "side.settings.configurator", - ToolbarControlId::OpenConfigFile => "side.settings.config-file", + Some(match id { + ToolbarControlId::SettingsContextAwareUi => ids::SIDE_SETTINGS_CONTEXT_AWARE_UI, + ToolbarControlId::SettingsTextControls => ids::SIDE_SETTINGS_TEXT_CONTROLS, + ToolbarControlId::SettingsStatusBar => ids::SIDE_SETTINGS_STATUS_BAR, + ToolbarControlId::SettingsStatusBoardBadge => ids::SIDE_SETTINGS_STATUS_BOARD_BADGE, + ToolbarControlId::SettingsStatusPageBadge => ids::SIDE_SETTINGS_STATUS_PAGE_BADGE, + ToolbarControlId::SettingsFloatingBadgeAlways => ids::SIDE_SETTINGS_FLOATING_BADGE_ALWAYS, + ToolbarControlId::SettingsPresetToasts => ids::SIDE_SETTINGS_PRESET_TOASTS, + ToolbarControlId::SettingsPresets => ids::SIDE_SETTINGS_PRESETS, + ToolbarControlId::SettingsActions => ids::SIDE_SETTINGS_ACTIONS, + ToolbarControlId::SettingsZoomActions => ids::SIDE_SETTINGS_ZOOM_ACTIONS, + ToolbarControlId::SettingsAdvancedActions => ids::SIDE_SETTINGS_ADVANCED_ACTIONS, + ToolbarControlId::SettingsBoards => ids::SIDE_SETTINGS_BOARDS, + ToolbarControlId::SettingsPages => ids::SIDE_SETTINGS_PAGES, + ToolbarControlId::SettingsStepControls => ids::SIDE_SETTINGS_STEP_CONTROLS, + ToolbarControlId::OpenConfigurator => ids::SIDE_SETTINGS_CONFIGURATOR, + ToolbarControlId::OpenConfigFile => ids::SIDE_SETTINGS_CONFIG_FILE, _ => return None, - })) + }) } #[derive(Debug, Clone)] @@ -403,22 +472,66 @@ pub(crate) struct ToolbarSettingsItemOverride { pub(crate) shown: bool, pub(crate) activation: ToolbarActivation, pub(crate) tooltip: ToolbarTooltip, + pub(crate) order: Option, } impl ToolbarSettingsItemOverride { - fn new(snapshot: &ToolbarSnapshot, definition: &ToolbarItemDefinition) -> Self { + fn new( + snapshot: &ToolbarSnapshot, + group: ToolbarItemCustomizeGroup, + definition: &ToolbarItemDefinition, + ) -> Self { let id = definition.id; let hidden = snapshot.toolbar_item_hidden(id); + let order = + definition_order_group_for_customize(group, definition).and_then(|order_group| { + let index = snapshot + .resolved_toolbar_items + .order + .index_of(order_group, id)?; + let len = snapshot + .resolved_toolbar_items + .order + .ordered_ids(order_group) + .len(); + Some(ToolbarSettingsItemOrder { + group: order_group, + index, + can_move_up: index > 0, + can_move_down: index + 1 < len, + move_up: ToolbarActivation::Click(ToolbarEvent::MoveToolbarItem { + group: order_group, + id, + delta: -1, + }), + move_down: ToolbarActivation::Click(ToolbarEvent::MoveToolbarItem { + group: order_group, + id, + delta: 1, + }), + }) + }); Self { id, label: Cow::Borrowed(definition.label), shown: !hidden, activation: ToolbarActivation::Click(ToolbarEvent::SetToolbarItemHidden(id, !hidden)), tooltip: ToolbarTooltip::text(format!("{}: uncheck to hide", definition.label)), + order, } } } +#[derive(Debug, Clone)] +pub(crate) struct ToolbarSettingsItemOrder { + pub(crate) group: ToolbarItemOrderGroup, + pub(crate) index: usize, + pub(crate) can_move_up: bool, + pub(crate) can_move_down: bool, + pub(crate) move_up: ToolbarActivation, + pub(crate) move_down: ToolbarActivation, +} + #[derive(Debug, Clone)] pub(crate) struct ToolbarSettingsToggle { pub(crate) id: ToolbarControlId, diff --git a/src/ui/toolbar/model/tools.rs b/src/ui/toolbar/model/tools.rs index 167ad021..b1e34b34 100644 --- a/src/ui/toolbar/model/tools.rs +++ b/src/ui/toolbar/model/tools.rs @@ -1,6 +1,6 @@ -use crate::config::ToolbarItemId; +use crate::config::{ToolbarItemId, ToolbarItemOrderGroup, toolbar_item_ids as ids}; use crate::input::Tool; -use crate::ui::toolbar::ToolbarSnapshot; +use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SemanticToolIcon { @@ -43,14 +43,6 @@ const FULL_TOOL_BUTTONS: [Tool; 10] = [ Tool::Blur, ]; -const COMMON_SHAPE_TOOLS: [Tool; 5] = [ - Tool::Line, - Tool::Rect, - Tool::Ellipse, - Tool::Arrow, - Tool::Blur, -]; - const SHAPE_TOOLS: [Tool; 10] = [ Tool::Line, Tool::Rect, @@ -64,6 +56,8 @@ const SHAPE_TOOLS: [Tool; 10] = [ Tool::FreeformPolygon, ]; +const SHAPE_PICKER_ROW_LEN: usize = 5; + const POLYGON_TOOLS: [Tool; 5] = [ Tool::Triangle, Tool::Parallelogram, @@ -91,10 +85,9 @@ pub(crate) fn visible_tools<'a>( tools: &'static [Tool], snapshot: &'a ToolbarSnapshot, ) -> impl Iterator + 'a { - tools - .iter() - .copied() - .filter(move |tool| tool_visible(snapshot, *tool)) + ordered_tools(snapshot) + .into_iter() + .filter(move |tool| tools.contains(tool) && tool_visible(snapshot, *tool)) } pub(crate) fn visible_tool_count(tools: &'static [Tool], snapshot: &ToolbarSnapshot) -> usize { @@ -102,86 +95,248 @@ pub(crate) fn visible_tool_count(tools: &'static [Tool], snapshot: &ToolbarSnaps } pub(crate) fn tool_visible(snapshot: &ToolbarSnapshot, tool: Tool) -> bool { - toolbar_item_visible(snapshot, toolbar_item_id_for_tool(tool).as_str()) + toolbar_item_visible(snapshot, toolbar_item_id_for_tool(tool)) } -pub(crate) fn toolbar_item_visible(snapshot: &ToolbarSnapshot, id: &'static str) -> bool { - !snapshot.toolbar_item_hidden(ToolbarItemId::from_known(id)) +pub(crate) fn toolbar_item_visible(snapshot: &ToolbarSnapshot, id: ToolbarItemId) -> bool { + !snapshot.toolbar_item_hidden(id) } pub(crate) fn top_shape_picker_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.shape-picker") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_SHAPE_PICKER) } pub(crate) fn top_fill_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.fill") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_FILL) } pub(crate) fn top_text_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.text") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_TEXT) } pub(crate) fn top_sticky_note_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.sticky-note") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_STICKY_NOTE) } pub(crate) fn top_clear_canvas_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.clear-canvas") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_CLEAR_CANVAS) } pub(crate) fn top_screenshot_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.screenshot") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_SCREENSHOT) } pub(crate) fn top_highlight_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.highlight") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_HIGHLIGHT) } pub(crate) fn top_highlight_ring_visible(snapshot: &ToolbarSnapshot) -> bool { - toolbar_item_visible(snapshot, "top.utility.highlight-ring") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_HIGHLIGHT_RING) } pub(crate) fn top_icon_mode_toggle_visible(snapshot: &ToolbarSnapshot) -> bool { if snapshot.use_icons { - toolbar_item_visible(snapshot, "top.utility.icon-mode-text") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_ICON_MODE_TEXT) } else { - toolbar_item_visible(snapshot, "top.utility.icon-mode-icons") + toolbar_item_visible(snapshot, ids::TOP_UTILITY_ICON_MODE_ICONS) } } -fn toolbar_item_id_for_tool(tool: Tool) -> ToolbarItemId { - ToolbarItemId::from_known(match tool { - Tool::Select => "top.tool.select", - Tool::Pen => "top.tool.pen", - Tool::Line => "top.tool.line", - Tool::Rect => "top.tool.rect", - Tool::Ellipse => "top.tool.ellipse", - Tool::Triangle => "top.tool.triangle", - Tool::Parallelogram => "top.tool.parallelogram", - Tool::Rhombus => "top.tool.rhombus", - Tool::RegularPolygon => "top.tool.regular-polygon", - Tool::FreeformPolygon => "top.tool.freeform-polygon", - Tool::Arrow => "top.tool.arrow", - Tool::Blur => "top.tool.blur", - Tool::Marker => "top.tool.marker", - Tool::Highlight => "top.utility.highlight", - Tool::StepMarker => "top.tool.step-marker", - Tool::Eraser => "top.tool.eraser", - }) +pub(crate) fn toolbar_item_id_for_tool(tool: Tool) -> ToolbarItemId { + match tool { + Tool::Select => ids::TOP_TOOL_SELECT, + Tool::Pen => ids::TOP_TOOL_PEN, + Tool::Line => ids::TOP_TOOL_LINE, + Tool::Rect => ids::TOP_TOOL_RECT, + Tool::Ellipse => ids::TOP_TOOL_ELLIPSE, + Tool::Triangle => ids::TOP_TOOL_TRIANGLE, + Tool::Parallelogram => ids::TOP_TOOL_PARALLELOGRAM, + Tool::Rhombus => ids::TOP_TOOL_RHOMBUS, + Tool::RegularPolygon => ids::TOP_TOOL_REGULAR_POLYGON, + Tool::FreeformPolygon => ids::TOP_TOOL_FREEFORM_POLYGON, + Tool::Arrow => ids::TOP_TOOL_ARROW, + Tool::Blur => ids::TOP_TOOL_BLUR, + Tool::Marker => ids::TOP_TOOL_MARKER, + Tool::Highlight => ids::TOP_UTILITY_HIGHLIGHT, + Tool::StepMarker => ids::TOP_TOOL_STEP_MARKER, + Tool::Eraser => ids::TOP_TOOL_ERASER, + } } -pub(crate) fn shape_tools() -> &'static [Tool] { - &SHAPE_TOOLS +fn tool_for_toolbar_item_id(id: ToolbarItemId) -> Option { + [ + (ids::TOP_TOOL_SELECT, Tool::Select), + (ids::TOP_TOOL_PEN, Tool::Pen), + (ids::TOP_TOOL_LINE, Tool::Line), + (ids::TOP_TOOL_RECT, Tool::Rect), + (ids::TOP_TOOL_ELLIPSE, Tool::Ellipse), + (ids::TOP_TOOL_TRIANGLE, Tool::Triangle), + (ids::TOP_TOOL_PARALLELOGRAM, Tool::Parallelogram), + (ids::TOP_TOOL_RHOMBUS, Tool::Rhombus), + (ids::TOP_TOOL_REGULAR_POLYGON, Tool::RegularPolygon), + (ids::TOP_TOOL_FREEFORM_POLYGON, Tool::FreeformPolygon), + (ids::TOP_TOOL_ARROW, Tool::Arrow), + (ids::TOP_TOOL_BLUR, Tool::Blur), + (ids::TOP_TOOL_MARKER, Tool::Marker), + (ids::TOP_TOOL_STEP_MARKER, Tool::StepMarker), + (ids::TOP_TOOL_ERASER, Tool::Eraser), + ] + .into_iter() + .find_map(|(candidate, tool)| (candidate == id).then_some(tool)) +} + +fn ordered_tools(snapshot: &ToolbarSnapshot) -> Vec { + snapshot + .resolved_toolbar_items + .order + .ordered_ids(ToolbarItemOrderGroup::TopTools) + .iter() + .filter_map(|id| tool_for_toolbar_item_id(*id)) + .collect() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TopUtilityButton { + Text, + StickyNote, + Screenshot, + ClearCanvas, + Highlight, + IconMode, +} + +const DEFAULT_TOP_UTILITY_BUTTONS: [TopUtilityButton; 5] = [ + TopUtilityButton::Text, + TopUtilityButton::StickyNote, + TopUtilityButton::Screenshot, + TopUtilityButton::ClearCanvas, + TopUtilityButton::Highlight, +]; + +impl TopUtilityButton { + pub(crate) fn id(self, snapshot: &ToolbarSnapshot) -> ToolbarItemId { + match self { + Self::Text => ids::TOP_UTILITY_TEXT, + Self::StickyNote => ids::TOP_UTILITY_STICKY_NOTE, + Self::Screenshot => ids::TOP_UTILITY_SCREENSHOT, + Self::ClearCanvas => ids::TOP_UTILITY_CLEAR_CANVAS, + Self::Highlight => ids::TOP_UTILITY_HIGHLIGHT, + Self::IconMode if snapshot.use_icons => ids::TOP_UTILITY_ICON_MODE_TEXT, + Self::IconMode => ids::TOP_UTILITY_ICON_MODE_ICONS, + } + } +} + +pub(crate) fn visible_top_utility_buttons( + snapshot: &ToolbarSnapshot, + simple: bool, + use_icons: bool, +) -> Vec { + let mut ordered = Vec::with_capacity(DEFAULT_TOP_UTILITY_BUTTONS.len()); + for id in snapshot + .resolved_toolbar_items + .order + .ordered_ids(ToolbarItemOrderGroup::TopControls) + { + if let Some(button) = top_utility_button_for_id(*id, snapshot) + && top_utility_button_visible(snapshot, button, simple, use_icons) + && !ordered.contains(&button) + { + ordered.push(button); + } + } + for button in DEFAULT_TOP_UTILITY_BUTTONS { + if top_utility_button_visible(snapshot, button, simple, use_icons) + && !ordered.contains(&button) + { + ordered.push(button); + } + } + ordered } -pub(crate) fn common_shape_tools() -> &'static [Tool] { - &COMMON_SHAPE_TOOLS +fn top_utility_button_for_id( + id: ToolbarItemId, + snapshot: &ToolbarSnapshot, +) -> Option { + if id == ids::TOP_UTILITY_TEXT { + Some(TopUtilityButton::Text) + } else if id == ids::TOP_UTILITY_STICKY_NOTE { + Some(TopUtilityButton::StickyNote) + } else if id == ids::TOP_UTILITY_SCREENSHOT { + Some(TopUtilityButton::Screenshot) + } else if id == ids::TOP_UTILITY_CLEAR_CANVAS { + Some(TopUtilityButton::ClearCanvas) + } else if id == ids::TOP_UTILITY_HIGHLIGHT { + Some(TopUtilityButton::Highlight) + } else if id == ids::TOP_UTILITY_ICON_MODE_ICONS + || id == ids::TOP_UTILITY_ICON_MODE_TEXT + || id == TopUtilityButton::IconMode.id(snapshot) + { + Some(TopUtilityButton::IconMode) + } else { + None + } +} + +fn top_utility_button_visible( + snapshot: &ToolbarSnapshot, + button: TopUtilityButton, + simple: bool, + use_icons: bool, +) -> bool { + match button { + TopUtilityButton::Text => top_text_visible(snapshot), + TopUtilityButton::StickyNote => top_sticky_note_visible(snapshot), + TopUtilityButton::Screenshot => top_screenshot_visible(snapshot), + TopUtilityButton::ClearCanvas => !simple && top_clear_canvas_visible(snapshot), + TopUtilityButton::Highlight => !simple && use_icons && top_highlight_visible(snapshot), + TopUtilityButton::IconMode => false, + } +} + +pub(crate) fn shape_tools() -> &'static [Tool] { + &SHAPE_TOOLS } pub(crate) fn polygon_tools() -> &'static [Tool] { &POLYGON_TOOLS } +pub(crate) fn visible_shape_picker_rows( + snapshot: &ToolbarSnapshot, + is_simple: bool, +) -> Vec> { + visible_tools(shape_picker_tools(is_simple), snapshot) + .collect::>() + .chunks(SHAPE_PICKER_ROW_LEN) + .map(|chunk| chunk.to_vec()) + .collect() +} + +pub(crate) fn visible_shape_picker_row_count(snapshot: &ToolbarSnapshot, is_simple: bool) -> usize { + visible_tools(shape_picker_tools(is_simple), snapshot) + .count() + .div_ceil(SHAPE_PICKER_ROW_LEN) +} + +pub(crate) fn visible_shape_picker_max_row_len( + snapshot: &ToolbarSnapshot, + is_simple: bool, +) -> usize { + visible_tools(shape_picker_tools(is_simple), snapshot) + .count() + .min(SHAPE_PICKER_ROW_LEN) +} + +fn shape_picker_tools(is_simple: bool) -> &'static [Tool] { + if is_simple { + &SHAPE_TOOLS + } else { + &POLYGON_TOOLS + } +} + pub(crate) fn is_polygon_tool(tool: Tool) -> bool { polygon_tools().contains(&tool) } @@ -238,6 +393,51 @@ pub(crate) fn fill_tool_active(active_tool: Tool, tool_override: Option) - tool_override.is_some_and(is_fill_tool) || is_fill_tool(active_tool) } +pub(crate) fn ordered_side_sections(snapshot: &ToolbarSnapshot) -> Vec { + snapshot + .resolved_toolbar_items + .order + .ordered_ids(ToolbarItemOrderGroup::SideSections) + .iter() + .filter_map(|id| side_section_for_toolbar_item_id(*id)) + .collect() +} + +fn side_section_for_toolbar_item_id(id: ToolbarItemId) -> Option { + [ + (ids::SIDE_GROUP_COLORS, ToolbarSideSection::Colors), + (ids::SIDE_GROUP_PRESETS, ToolbarSideSection::Presets), + (ids::SIDE_GROUP_THICKNESS, ToolbarSideSection::Thickness), + (ids::SIDE_GROUP_ERASER_MODE, ToolbarSideSection::EraserMode), + ( + ids::SIDE_GROUP_POLYGON_SIDES, + ToolbarSideSection::PolygonSides, + ), + ( + ids::SIDE_GROUP_ARROW_LABELS, + ToolbarSideSection::ArrowLabels, + ), + ( + ids::SIDE_GROUP_STEP_MARKERS, + ToolbarSideSection::StepMarkers, + ), + ( + ids::SIDE_GROUP_MARKER_OPACITY, + ToolbarSideSection::MarkerOpacity, + ), + (ids::SIDE_GROUP_TEXT_SIZE, ToolbarSideSection::TextSize), + (ids::SIDE_GROUP_FONT, ToolbarSideSection::Font), + (ids::SIDE_GROUP_ACTIONS, ToolbarSideSection::Actions), + (ids::SIDE_GROUP_BOARDS, ToolbarSideSection::Boards), + (ids::SIDE_GROUP_PAGES, ToolbarSideSection::Pages), + (ids::SIDE_GROUP_STEP_UNDO, ToolbarSideSection::StepUndo), + (ids::SIDE_GROUP_SESSION, ToolbarSideSection::Session), + (ids::SIDE_GROUP_SETTINGS, ToolbarSideSection::Settings), + ] + .into_iter() + .find_map(|(candidate, section)| (candidate == id).then_some(section)) +} + pub(crate) fn current_shape_tool(active_tool: Tool, tool_override: Option) -> Option { tool_override .filter(|tool| is_shape_tool(*tool)) diff --git a/src/ui/toolbar/snapshot/types.rs b/src/ui/toolbar/snapshot/types.rs index 262f94fe..1aea0520 100644 --- a/src/ui/toolbar/snapshot/types.rs +++ b/src/ui/toolbar/snapshot/types.rs @@ -1,4 +1,6 @@ -use crate::config::{ResolvedToolbarItems, ToolbarGroupId, ToolbarItemId, ToolbarLayoutMode}; +use crate::config::{ + ResolvedToolbarItems, ToolbarGroupId, ToolbarItemId, ToolbarLayoutMode, toolbar_item_ids as ids, +}; use crate::draw::{Color, EraserKind, FontDescriptor}; use crate::input::state::PresetFeedbackKind; use crate::input::tool::{ToolControlGroup, ToolProfile}; @@ -371,15 +373,15 @@ impl ToolbarSnapshot { ToolbarSideSection::Settings => ToolbarGroupId::Settings, }; let legacy_item = match section { - ToolbarSideSection::Colors => Some("side.tool-options.color"), - ToolbarSideSection::Thickness => Some("side.tool-options.thickness"), - ToolbarSideSection::EraserMode => Some("side.tool-options.eraser-mode"), - ToolbarSideSection::PolygonSides => Some("side.tool-options.polygon-sides"), - ToolbarSideSection::ArrowLabels => Some("side.tool-options.arrow-labels"), - ToolbarSideSection::StepMarkers => Some("side.tool-options.step-marker-reset"), - ToolbarSideSection::MarkerOpacity => Some("side.tool-options.marker-opacity"), - ToolbarSideSection::TextSize => Some("side.tool-options.font-size"), - ToolbarSideSection::Font => Some("side.tool-options.font-family"), + ToolbarSideSection::Colors => Some(ids::SIDE_TOOL_OPTIONS_COLOR), + ToolbarSideSection::Thickness => Some(ids::SIDE_TOOL_OPTIONS_THICKNESS), + ToolbarSideSection::EraserMode => Some(ids::SIDE_TOOL_OPTIONS_ERASER_MODE), + ToolbarSideSection::PolygonSides => Some(ids::SIDE_TOOL_OPTIONS_POLYGON_SIDES), + ToolbarSideSection::ArrowLabels => Some(ids::SIDE_TOOL_OPTIONS_ARROW_LABELS), + ToolbarSideSection::StepMarkers => Some(ids::SIDE_TOOL_OPTIONS_STEP_MARKER_RESET), + ToolbarSideSection::MarkerOpacity => Some(ids::SIDE_TOOL_OPTIONS_MARKER_OPACITY), + ToolbarSideSection::TextSize => Some(ids::SIDE_TOOL_OPTIONS_FONT_SIZE), + ToolbarSideSection::Font => Some(ids::SIDE_TOOL_OPTIONS_FONT_FAMILY), ToolbarSideSection::Presets | ToolbarSideSection::Actions | ToolbarSideSection::Boards @@ -390,8 +392,7 @@ impl ToolbarSnapshot { }; self.toolbar_group_hidden(group) - || legacy_item - .is_some_and(|item| self.toolbar_item_hidden(ToolbarItemId::from_known(item))) + || legacy_item.is_some_and(|item| self.toolbar_item_hidden(item)) } pub fn side_section_collapsed(&self, section: ToolbarSideSection) -> bool {