diff --git a/config.example.toml b/config.example.toml index 74c086c0..d2092f76 100644 --- a/config.example.toml +++ b/config.example.toml @@ -420,6 +420,13 @@ side_offset_x = 0.0 # Force inline toolbars even when layer-shell is available force_inline = false +[ui.toolbar.items] +# Checked in the configurator means shown; IDs listed here are hidden. +# The screenshot toolbar button is hidden by default. +hidden = [ + "top.utility.screenshot", +] + # ─────────────────────────────────────────────────────────────────────────────── # Status Bar Styling # ─────────────────────────────────────────────────────────────────────────────── diff --git a/configurator/src/app/search/summary.rs b/configurator/src/app/search/summary.rs index 98b66fdc..1bfd35ab 100644 --- a/configurator/src/app/search/summary.rs +++ b/configurator/src/app/search/summary.rs @@ -1,5 +1,6 @@ use crate::app::state::ConfiguratorApp; use crate::models::{KeybindingsTabId, SearchQuery, TabId, UiTabId}; +use wayscriber::config::toolbar_item_definitions; use super::terms::*; use super::types::{AppSearchSummary, SearchArea, TabSearchSummary}; @@ -178,6 +179,7 @@ fn ui_matches(query: &SearchQuery, summary: &mut TabSearchSummary) { .collect::>(); if query.matches_parts_scoped_to_tab(TabId::Ui, identity_parts.iter().copied()) || query.matches_parts(full_parts.iter().copied()) + || (tab == UiTabId::ToolbarVisibility && toolbar_item_matches(query)) || (query.matches_any_raw_text(field_terms) && query.matches_parts_scoped_to_tab(TabId::Ui, full_parts.iter().copied())) { @@ -186,6 +188,26 @@ fn ui_matches(query: &SearchQuery, summary: &mut TabSearchSummary) { } } +fn toolbar_item_matches(query: &SearchQuery) -> bool { + toolbar_item_definitions().iter().any(|definition| { + query.matches_parts([ + "toolbar item", + definition.label, + definition.id.as_str(), + definition.group.map_or("", |group| group.as_str()), + ]) || query.matches_parts_scoped_to_tab( + TabId::Ui, + [ + "toolbar visibility", + "toolbar item", + definition.label, + definition.id.as_str(), + definition.group.map_or("", |group| group.as_str()), + ], + ) + }) +} + fn board_matches(app: &ConfiguratorApp, query: &SearchQuery, summary: &mut TabSearchSummary) { add_area_if( query, diff --git a/configurator/src/app/search/terms.rs b/configurator/src/app/search/terms.rs index 5cd56376..d09b7655 100644 --- a/configurator/src/app/search/terms.rs +++ b/configurator/src/app/search/terms.rs @@ -47,6 +47,7 @@ pub(super) fn tab_scope_aliases(tab: TabId) -> &'static [&'static str] { pub(super) fn ui_tab_aliases(tab: UiTabId) -> &'static [&'static str] { match tab { UiTabId::Toolbar => &["tools", "palette", "drawer"], + UiTabId::ToolbarVisibility => &["toolbar items", "toolbar buttons", "show hide toolbar"], UiTabId::StatusBar => &["status", "badge", "indicator"], UiTabId::HelpOverlay => &["help", "quick help", "hints"], UiTabId::ClickHighlight => &["click", "cursor", "highlight"], @@ -57,6 +58,7 @@ pub(super) fn ui_tab_aliases(tab: UiTabId) -> &'static [&'static str] { pub(super) fn ui_tab_terms(tab: UiTabId) -> &'static [&'static str] { match tab { UiTabId::Toolbar => UI_TOOLBAR_TERMS, + UiTabId::ToolbarVisibility => UI_TOOLBAR_VISIBILITY_TERMS, UiTabId::StatusBar => UI_STATUS_BAR_TERMS, UiTabId::HelpOverlay => UI_HELP_OVERLAY_TERMS, UiTabId::ClickHighlight => UI_CLICK_HIGHLIGHT_TERMS, @@ -193,6 +195,29 @@ pub(super) const UI_TOOLBAR_TERMS: &[&str] = &[ "side offset x", "side offset y", ]; +pub(super) const UI_TOOLBAR_VISIBILITY_TERMS: &[&str] = &[ + "toolbar visibility", + "toolbar items", + "items", + "checked items are shown", + "hidden item overrides", + "hide toolbar buttons", + "show toolbar buttons", + "visible toolbar buttons", + "top toolbar", + "side toolbar", + "toolbar controls", + "tools", + "utilities", + "sections", + "actions", + "pages", + "boards", + "presets", + "settings", + "sessions", + "tool options", +]; pub(super) const UI_STATUS_BAR_TERMS: &[&str] = &[ "status bar", "show status bar", diff --git a/configurator/src/app/search/tests.rs b/configurator/src/app/search/tests.rs index ac4efbe2..81c8c157 100644 --- a/configurator/src/app/search/tests.rs +++ b/configurator/src/app/search/tests.rs @@ -259,13 +259,17 @@ fn ui_nested_alias_matches_concrete_nested_tab() { #[test] fn parent_scoped_ui_queries_match_concrete_nested_tabs() { let cases = [ - ("ui toolbar", UiTabId::Toolbar), - ("ui layout mode", UiTabId::Toolbar), - ("interface presenter", UiTabId::PresenterMode), - ("interface status bar position", UiTabId::StatusBar), + ( + "ui toolbar", + &[UiTabId::Toolbar, UiTabId::ToolbarVisibility][..], + ), + ("ui layout mode", &[UiTabId::Toolbar][..]), + ("ui toolbar blur", &[UiTabId::ToolbarVisibility][..]), + ("interface presenter", &[UiTabId::PresenterMode][..]), + ("interface status bar position", &[UiTabId::StatusBar][..]), ]; - for (query, expected_tab) in cases { + for (query, expected_tabs) in cases { let (mut app, _task) = ConfiguratorApp::new_app(); app.search_query = SearchQuery::new(query); @@ -275,7 +279,7 @@ fn parent_scoped_ui_queries_match_concrete_nested_tabs() { assert!(!ui.show_all(), "query should not show all UI tabs: {query}"); assert_eq!( ui.ui_tabs(), - &[expected_tab], + expected_tabs, "query should show concrete nested UI tab: {query}", ); } @@ -285,6 +289,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), ("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 a37fdf5c..dc001192 100644 --- a/configurator/src/app/update/fields.rs +++ b/configurator/src/app/update/fields.rs @@ -12,6 +12,7 @@ use crate::models::{ }; #[cfg(feature = "tablet-input")] use crate::models::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption}; +use wayscriber::config::ToolbarItemId; use super::super::state::{ConfiguratorApp, StatusMessage}; @@ -168,6 +169,17 @@ impl ConfiguratorApp { Task::none() } + pub(super) fn handle_toolbar_item_visibility_changed( + &mut self, + id: ToolbarItemId, + visible: bool, + ) -> Task { + self.status = StatusMessage::idle(); + self.draft.set_toolbar_item_visible(id, visible); + 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 b9cb78f3..8be465fe 100644 --- a/configurator/src/app/update/mod.rs +++ b/configurator/src/app/update/mod.rs @@ -119,6 +119,9 @@ impl ConfiguratorApp { Message::ToolbarOverrideChanged(field, option) => { self.handle_toolbar_override_changed(field, option) } + Message::ToolbarItemVisibilityChanged(id, visible) => { + self.handle_toolbar_item_visibility_changed(id, visible) + } 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/mod.rs b/configurator/src/app/view/ui/mod.rs index cafabd18..fb069e05 100644 --- a/configurator/src/app/view/ui/mod.rs +++ b/configurator/src/app/view/ui/mod.rs @@ -38,6 +38,7 @@ impl ConfiguratorApp { let content = match active_tab { Some(UiTabId::Toolbar) => Some(self.ui_toolbar_tab()), + Some(UiTabId::ToolbarVisibility) => Some(self.ui_toolbar_visibility_tab()), Some(UiTabId::StatusBar) => Some(self.ui_status_bar_tab()), Some(UiTabId::HelpOverlay) => Some(self.ui_help_overlay_tab()), Some(UiTabId::ClickHighlight) => Some(self.ui_click_highlight_tab()), diff --git a/configurator/src/app/view/ui/toolbar.rs b/configurator/src/app/view/ui/toolbar.rs index fa73b9aa..5b5c117d 100644 --- a/configurator/src/app/view/ui/toolbar.rs +++ b/configurator/src/app/view/ui/toolbar.rs @@ -1,5 +1,9 @@ -use iced::widget::{column, pick_list, row, scrollable, text}; +use iced::widget::{checkbox, column, pick_list, row, scrollable, text}; use iced::{Element, Length}; +use wayscriber::config::{ + ResolvedToolbarItems, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemSurface, + ToolbarItemsConfig, toolbar_item_definitions, +}; use crate::app::scroll::CONTENT_SCROLL_ID; use crate::app::state::ConfiguratorApp; @@ -215,4 +219,109 @@ impl ConfiguratorApp { scrollable(column).id(CONTENT_SCROLL_ID).into() } + + pub(super) fn ui_toolbar_visibility_tab(&self) -> Element<'_, Message> { + let column = column![ + text("Toolbar Visibility").size(18), + text("Checked items are shown. Uncheck an item to hide it from toolbar sizing, drawing, and hit testing. Existing section toggles and mode overrides can still hide checked items.").size(12), + toolbar_item_visibility_section( + &self.draft.ui_toolbar_items, + &self.defaults.ui_toolbar_items, + ), + ] + .spacing(12); + + scrollable(column).id(CONTENT_SCROLL_ID).into() + } +} + +fn toolbar_item_visibility_section<'a>( + items: &ToolbarItemsConfig, + defaults: &ToolbarItemsConfig, +) -> Element<'a, Message> { + let resolved = items.resolved(); + let default_resolved = defaults.resolved(); + let mut rows = column![text("Items").size(16)].spacing(8); + let mut current_surface = None; + let mut current_category = None; + + if !resolved.unknown_hidden.is_empty() { + rows = rows.push( + text(format!( + "Preserving {} unknown toolbar item id(s) from config.", + resolved.unknown_hidden.len() + )) + .size(12), + ); + } + + for definition in toolbar_item_definitions() { + if current_surface != Some(definition.surface) { + current_surface = Some(definition.surface); + current_category = None; + rows = rows.push(text(toolbar_item_surface_label(definition.surface)).size(14)); + } + if current_category != Some(definition.category) { + current_category = Some(definition.category); + rows = rows.push(text(toolbar_item_category_label(definition.category)).size(13)); + } + + rows = rows.push(toolbar_item_visibility_row( + definition, + &resolved, + &default_resolved, + )); + } + + rows.into() +} + +fn toolbar_item_visibility_row<'a>( + definition: &ToolbarItemDefinition, + resolved: &ResolvedToolbarItems, + defaults: &ResolvedToolbarItems, +) -> Element<'a, Message> { + let id = definition.id; + let visible = !resolved.is_hidden(id); + let default = format!( + "default: {}", + visibility_override_label(!defaults.is_hidden(id)) + ); + + 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() +} + +fn visibility_override_label(visible: bool) -> &'static str { + if visible { "shown" } else { "hidden" } +} + +fn toolbar_item_surface_label(surface: ToolbarItemSurface) -> &'static str { + match surface { + ToolbarItemSurface::Top => "Top toolbar", + ToolbarItemSurface::Side => "Side toolbar", + } +} + +fn toolbar_item_category_label(category: ToolbarItemCategory) -> &'static str { + match category { + ToolbarItemCategory::Chrome => "Toolbar controls", + ToolbarItemCategory::Tool => "Tools", + ToolbarItemCategory::Utility => "Utilities", + ToolbarItemCategory::Group => "Sections", + ToolbarItemCategory::Action => "Actions", + ToolbarItemCategory::Page => "Pages", + ToolbarItemCategory::Board => "Boards", + ToolbarItemCategory::Setting => "Settings", + ToolbarItemCategory::Session => "Sessions", + ToolbarItemCategory::ToolOption => "Tool options", + } } diff --git a/configurator/src/messages.rs b/configurator/src/messages.rs index 17fde4f2..f21c6e4d 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; +use wayscriber::config::{Config, ToolbarItemId}; use crate::models::{ BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode, ColorPickerId, @@ -73,6 +73,7 @@ pub enum Message { ToolbarLayoutModeChanged(ToolbarLayoutModeOption), ToolbarOverrideModeChanged(ToolbarLayoutModeOption), ToolbarOverrideChanged(ToolbarOverrideField, OverrideOption), + ToolbarItemVisibilityChanged(ToolbarItemId, bool), BoardsAddItem, BoardsRemoveItem(usize), BoardsMoveItemUp(usize), diff --git a/configurator/src/models/config/draft/from_config.rs b/configurator/src/models/config/draft/from_config.rs index d8834b30..2f5cfa57 100644 --- a/configurator/src/models/config/draft/from_config.rs +++ b/configurator/src/models/config/draft/from_config.rs @@ -101,6 +101,7 @@ impl ConfigDraft { ui_toolbar_layout_mode: ToolbarLayoutModeOption::from_mode( config.ui.toolbar.layout_mode, ), + ui_toolbar_items: config.ui.toolbar.items.clone(), ui_toolbar_show_presets: config.ui.toolbar.show_presets, ui_toolbar_show_actions_section: config.ui.toolbar.show_actions_section, ui_toolbar_show_actions_advanced: config.ui.toolbar.show_actions_advanced, diff --git a/configurator/src/models/config/draft/mod.rs b/configurator/src/models/config/draft/mod.rs index 42ca26b8..aa63b0ee 100644 --- a/configurator/src/models/config/draft/mod.rs +++ b/configurator/src/models/config/draft/mod.rs @@ -14,7 +14,7 @@ use super::boards::BoardsDraft; use super::presets::PresetsDraft; use super::render_profiles::RenderProfilesDraft; use super::toolbar_overrides::ToolbarModeOverridesDraft; -use wayscriber::config::MouseDragToolsConfig; +use wayscriber::config::{MouseDragToolsConfig, ToolbarItemsConfig}; #[derive(Debug, Clone, PartialEq)] pub struct ConfigDraft { @@ -76,6 +76,7 @@ pub struct ConfigDraft { pub ui_toolbar_show_more_colors: bool, pub ui_toolbar_show_preset_toasts: bool, pub ui_toolbar_layout_mode: ToolbarLayoutModeOption, + pub ui_toolbar_items: ToolbarItemsConfig, pub ui_toolbar_show_presets: bool, pub ui_toolbar_show_actions_section: bool, pub ui_toolbar_show_actions_advanced: bool, diff --git a/configurator/src/models/config/setters.rs b/configurator/src/models/config/setters.rs index 8805e0d3..5ebb6313 100644 --- a/configurator/src/models/config/setters.rs +++ b/configurator/src/models/config/setters.rs @@ -4,6 +4,7 @@ use super::super::fields::{ ToolbarOverrideField, TripletField, }; use super::draft::ConfigDraft; +use wayscriber::config::ToolbarItemId; impl ConfigDraft { pub fn apply_toolbar_layout_mode(&mut self, mode: ToolbarLayoutModeOption) { @@ -31,6 +32,10 @@ impl ConfigDraft { .set(field, value); } + pub fn set_toolbar_item_visible(&mut self, id: ToolbarItemId, visible: bool) { + self.ui_toolbar_items.set_hidden(id, !visible); + } + 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 f0003360..27c640cf 100644 --- a/configurator/src/models/config/tests.rs +++ b/configurator/src/models/config/tests.rs @@ -10,7 +10,8 @@ use super::{ConfigDraft, RenderProfileSelectionOption}; use wayscriber::config::{ ColorSpec, Config, PdfFitMode, PdfLabelContentMode, PdfLabelPosition, PdfOrientation, PdfPageSize, PdfTransparentBackground, PresetToolStatesConfig, RenderColorMappingConfig, - RenderProfileConfig, RenderProfileExportMode, ToolPresetConfig, XdgFocusLossBehavior, + RenderProfileConfig, RenderProfileExportMode, ToolPresetConfig, ToolbarItemsConfig, + XdgFocusLossBehavior, }; use wayscriber::input::{DragTool, PerToolDrawingSettings, Tool}; @@ -62,6 +63,31 @@ fn config_draft_round_trips_light_mode_click_highlight_policy() { assert!(!round_trip.ui.click_highlight.force_in_light_mode); } +#[test] +fn config_draft_round_trips_toolbar_item_visibility_preserving_unknown_ids() { + let mut config = Config::default(); + config.ui.toolbar.items = ToolbarItemsConfig { + hidden: vec![ + "future.toolbar.item".to_string(), + "side.actions.undo-all".to_string(), + "side.actions.undo-all".to_string(), + ], + }; + + 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); + + let round_trip = draft + .to_config(&config) + .expect("expected config to round trip"); + + assert_eq!( + round_trip.ui.toolbar.items.hidden, + vec!["future.toolbar.item", "top.tool.pen"] + ); +} + #[test] fn config_draft_round_trips_render_profiles() { let mut config = Config::default(); diff --git a/configurator/src/models/config/to_config/ui.rs b/configurator/src/models/config/to_config/ui.rs index aea95cbf..7f11f8cd 100644 --- a/configurator/src/models/config/to_config/ui.rs +++ b/configurator/src/models/config/to_config/ui.rs @@ -37,6 +37,7 @@ impl ConfigDraft { config.ui.toolbar.show_preset_toasts = self.ui_toolbar_show_preset_toasts; config.ui.toolbar.layout_mode = self.ui_toolbar_layout_mode.to_mode(); config.ui.toolbar.mode_overrides = self.ui_toolbar_mode_overrides.to_config(); + config.ui.toolbar.items = self.ui_toolbar_items.clone(); config.ui.toolbar.show_presets = self.ui_toolbar_show_presets; config.ui.toolbar.show_actions_section = self.ui_toolbar_show_actions_section; config.ui.toolbar.show_actions_advanced = self.ui_toolbar_show_actions_advanced; diff --git a/configurator/src/models/tab.rs b/configurator/src/models/tab.rs index fc85cca9..cd1ce319 100644 --- a/configurator/src/models/tab.rs +++ b/configurator/src/models/tab.rs @@ -73,6 +73,7 @@ impl TabId { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UiTabId { Toolbar, + ToolbarVisibility, StatusBar, HelpOverlay, ClickHighlight, @@ -80,8 +81,9 @@ pub enum UiTabId { } impl UiTabId { - pub const ALL: [UiTabId; 5] = [ + pub const ALL: [UiTabId; 6] = [ UiTabId::Toolbar, + UiTabId::ToolbarVisibility, UiTabId::StatusBar, UiTabId::HelpOverlay, UiTabId::ClickHighlight, @@ -91,6 +93,7 @@ impl UiTabId { pub fn title(&self) -> &'static str { match self { UiTabId::Toolbar => "Toolbar", + UiTabId::ToolbarVisibility => "Toolbar Visibility", UiTabId::StatusBar => "Status Bar", UiTabId::HelpOverlay => "Help Overlay", UiTabId::ClickHighlight => "Click Highlight", diff --git a/docs/CONFIG.md b/docs/CONFIG.md index a1ba3dc5..f2c395bd 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -555,6 +555,17 @@ side_offset_x = 0.0 # Force inline toolbars even when layer-shell is available force_inline = false + +[ui.toolbar.items] +# Hide individual toolbar items or whole side sections by stable ID. +# Unknown IDs are warned about but preserved across toolbar saves. +hidden = [ + "top.utility.screenshot", + "top.tool.blur", + "top.utility.clear-canvas", + "side.actions.undo-all", + "side.group.presets", +] ``` **Behavior:** @@ -579,6 +590,8 @@ force_inline = false - **Offsets**: `top_offset`, `top_offset_y`, `side_offset`, `side_offset_x` store toolbar positions. - **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. +- **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/backend/state_init/input_state.rs b/src/backend/wayland/backend/state_init/input_state.rs index 787b0b23..f195f1c5 100644 --- a/src/backend/wayland/backend/state_init/input_state.rs +++ b/src/backend/wayland/backend/state_init/input_state.rs @@ -70,6 +70,7 @@ pub(super) fn build_input_state(config: &Config) -> InputState { input_state.init_toolbar_from_config( config.ui.toolbar.layout_mode, config.ui.toolbar.mode_overrides.clone(), + config.ui.toolbar.items.clone(), config.ui.toolbar.top_pinned, config.ui.toolbar.side_pinned, config.ui.toolbar.use_icons, diff --git a/src/backend/wayland/state/toolbar/events.rs b/src/backend/wayland/state/toolbar/events.rs index 94ac7bec..eeb4c404 100644 --- a/src/backend/wayland/state/toolbar/events.rs +++ b/src/backend/wayland/state/toolbar/events.rs @@ -194,6 +194,7 @@ impl WaylandState { /// Saves the current toolbar configuration to disk (pinned state, icon mode, section visibility). pub(super) fn save_toolbar_pin_config(&mut self) { self.config.ui.toolbar.layout_mode = self.input_state.toolbar_layout_mode; + self.config.ui.toolbar.items = self.input_state.toolbar_items.clone(); self.config.ui.toolbar.top_pinned = self.input_state.toolbar_top_pinned; self.config.ui.toolbar.side_pinned = self.input_state.toolbar_side_pinned; self.config.ui.toolbar.use_icons = self.input_state.toolbar_use_icons; diff --git a/src/backend/wayland/state/toolbar/events/session.rs b/src/backend/wayland/state/toolbar/events/session.rs index d4e43d6e..d2f48172 100644 --- a/src/backend/wayland/state/toolbar/events/session.rs +++ b/src/backend/wayland/state/toolbar/events/session.rs @@ -12,7 +12,7 @@ pub(super) fn populate_session_snapshot( snapshot.active_session_name = active_path.as_deref().map(session_display_name); snapshot.active_session_path = active_path.clone(); snapshot.recent_sessions = - if snapshot.drawer_open && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::App { + if snapshot.drawer_open && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Session { recent_session_snapshots(active_path.as_deref()) } else { Vec::new() diff --git a/src/backend/wayland/toolbar/layout/side/arrow.rs b/src/backend/wayland/toolbar/layout/side/arrow.rs index 7eb275f8..3b930a97 100644 --- a/src/backend/wayland/toolbar/layout/side/arrow.rs +++ b/src/backend/wayland/toolbar/layout/side/arrow.rs @@ -6,7 +6,11 @@ pub(super) fn push_arrow_section_hits( y: f64, hits: &mut Vec, ) -> f64 { - if !ToolContext::from_snapshot(ctx.snapshot).show_arrow_labels { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::ArrowLabels) + || !ToolContext::from_snapshot(ctx.snapshot).show_arrow_labels + { return y; } @@ -60,7 +64,11 @@ pub(super) fn push_step_marker_hits( y: f64, hits: &mut Vec, ) -> f64 { - if !ToolContext::from_snapshot(ctx.snapshot).show_step_counter { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::StepMarkers) + || !ToolContext::from_snapshot(ctx.snapshot).show_step_counter + { return y; } diff --git a/src/backend/wayland/toolbar/layout/side/colors.rs b/src/backend/wayland/toolbar/layout/side/colors.rs index 50f9140b..21c5d8df 100644 --- a/src/backend/wayland/toolbar/layout/side/colors.rs +++ b/src/backend/wayland/toolbar/layout/side/colors.rs @@ -6,6 +6,10 @@ pub(super) fn push_color_picker_hits( y: f64, hits: &mut Vec, ) -> f64 { + if ctx.snapshot.side_section_hidden(ToolbarSideSection::Colors) { + return y; + } + let card_h = ctx.spec.side_colors_height(ctx.snapshot); super::section_header::push_collapsible_header_hit(ctx, y, ToolbarSideSection::Colors, hits); if ctx diff --git a/src/backend/wayland/toolbar/layout/side/delay.rs b/src/backend/wayland/toolbar/layout/side/delay.rs index 151a5288..b7417451 100644 --- a/src/backend/wayland/toolbar/layout/side/delay.rs +++ b/src/backend/wayland/toolbar/layout/side/delay.rs @@ -9,7 +9,10 @@ pub(super) fn push_delay_hits( y: f64, hits: &mut Vec, ) -> f64 { - if !ctx.snapshot.show_step_section + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::StepUndo) + || !ctx.snapshot.show_step_section || !ctx.snapshot.drawer_open || ctx.snapshot.drawer_tab != crate::input::ToolbarDrawerTab::App { diff --git a/src/backend/wayland/toolbar/layout/side/drawer.rs b/src/backend/wayland/toolbar/layout/side/drawer.rs index 855948bb..6dc7cc02 100644 --- a/src/backend/wayland/toolbar/layout/side/drawer.rs +++ b/src/backend/wayland/toolbar/layout/side/drawer.rs @@ -14,11 +14,15 @@ pub(super) fn push_drawer_tabs_hits( let tab_y = y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; let tab_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; let tab_gap = ToolbarLayoutSpec::SIDE_TOGGLE_GAP; - let tab_w = (ctx.content_width - tab_gap) / 2.0; - let tabs = [ToolbarDrawerTab::View, ToolbarDrawerTab::App]; + let tabs = ToolbarDrawerTab::ALL; + let tab_columns = 3usize; + let tab_w = (ctx.content_width - tab_gap * (tab_columns - 1) as f64) / tab_columns as f64; for (idx, tab) in tabs.iter().enumerate() { - let tab_x = ctx.x + (tab_w + tab_gap) * idx as f64; + let tab_col = idx % tab_columns; + let tab_row = idx / tab_columns; + let tab_x = ctx.x + (tab_w + tab_gap) * tab_col as f64; + let tab_y = tab_y + (tab_h + tab_gap) * tab_row as f64; hits.push(HitRegion { rect: (tab_x, tab_y, tab_w, tab_h), event: ToolbarEvent::SetDrawerTab(*tab), diff --git a/src/backend/wayland/toolbar/layout/side/mod.rs b/src/backend/wayland/toolbar/layout/side/mod.rs index c4f9edbd..ac1177f5 100644 --- a/src/backend/wayland/toolbar/layout/side/mod.rs +++ b/src/backend/wayland/toolbar/layout/side/mod.rs @@ -18,7 +18,7 @@ pub(super) use super::super::hit::HitRegion; pub(super) use super::spec::ToolbarLayoutSpec; pub(super) use crate::ui::toolbar::model::{delay_secs_from_t, delay_t_from_ms}; pub(super) use crate::ui::toolbar::snapshot::ToolContext; -pub(super) use crate::ui::toolbar::{ToolbarEvent, ToolbarSnapshot}; +pub(super) use crate::ui::toolbar::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; /// Populate hit regions for the side toolbar. #[allow(dead_code)] @@ -35,45 +35,75 @@ pub fn build_side_hits( let mut y = ctx.spec.side_content_start_y(); + if snapshot.drawer_open + && (snapshot.customize_items_open + || snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Customize + || snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Session) + { + y = drawer::push_drawer_tabs_hits(&ctx, y, hits); + if snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Session { + session::push_session_hits(&ctx, y, hits); + } else { + settings::push_settings_hits(&ctx, y, hits); + } + return; + } + // Color section: only when tool needs color - if tool_context.needs_color { + if tool_context.needs_color && !snapshot.side_section_hidden(ToolbarSideSection::Colors) { y = colors::push_color_picker_hits(&ctx, y, hits); } - y = presets::push_preset_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 { + 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 { + 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 { + 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 { + 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 { + if tool_context.show_marker_opacity + && !snapshot.side_section_hidden(ToolbarSideSection::MarkerOpacity) + { y = sliders::push_marker_opacity_hits(&ctx, y, hits); } // Text controls: only when text/note is active if tool_context.show_font_controls { - y = sliders::push_text_size_hits(&ctx, y, hits); - y = sliders::push_font_hits(&ctx, y, hits); + 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); + } } y = drawer::push_drawer_tabs_hits(&ctx, y, hits); diff --git a/src/backend/wayland/toolbar/layout/side/presets.rs b/src/backend/wayland/toolbar/layout/side/presets.rs index 354dbbf3..a51ecced 100644 --- a/src/backend/wayland/toolbar/layout/side/presets.rs +++ b/src/backend/wayland/toolbar/layout/side/presets.rs @@ -6,6 +6,13 @@ pub(super) fn push_preset_hits( y: f64, hits: &mut Vec, ) -> f64 { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::Presets) + { + return y; + } + let slot_count = ctx .snapshot .preset_slot_count diff --git a/src/backend/wayland/toolbar/layout/side/settings.rs b/src/backend/wayland/toolbar/layout/side/settings.rs index da15c1d6..bee53212 100644 --- a/src/backend/wayland/toolbar/layout/side/settings.rs +++ b/src/backend/wayland/toolbar/layout/side/settings.rs @@ -9,9 +9,15 @@ pub(super) fn push_settings_hits(ctx: &SideLayoutContext<'_>, y: f64, hits: &mut }; super::section_header::push_collapsible_header_hit(ctx, y, ToolbarSideSection::Settings, hits); - if ctx - .snapshot - .side_section_collapsed(ToolbarSideSection::Settings) + let dedicated_panel = ctx.snapshot.customize_items_open + || matches!( + ctx.snapshot.drawer_tab, + crate::input::ToolbarDrawerTab::Sections | crate::input::ToolbarDrawerTab::Customize + ); + if !dedicated_panel + && ctx + .snapshot + .side_section_collapsed(ToolbarSideSection::Settings) { return; } @@ -69,6 +75,58 @@ pub(super) fn push_settings_hits(ctx: &SideLayoutContext<'_>, y: f64, hits: &mut tooltip: button.tooltip.as_string(), }); } + + let mut customize_y = buttons_y; + if button_layout.rows > 0 { + customize_y += button_layout.height; + } + customize_y += toggle_gap; + let groups = settings_model.groups(); + let group_layout = grid_layout( + ctx.x, + customize_y + toggle_h + toggle_gap, + button_w, + button_h, + button_gap, + button_gap, + 2, + groups.len(), + ); + for (item, group) in group_layout.items.iter().zip(groups.iter()) { + hits.push(HitRegion { + rect: (item.x, item.y, item.w, item.h), + event: group.event.clone(), + kind: HitKind::Click, + tooltip: group.tooltip.as_string(), + }); + } + + let mut items_y = customize_y; + if group_layout.rows > 0 { + items_y += toggle_h + toggle_gap + group_layout.height + toggle_gap; + } + let item_overrides = settings_model.item_overrides(); + if !item_overrides.is_empty() { + items_y += toggle_h + toggle_gap; + } + let item_layout = grid_layout( + ctx.x, + items_y, + ctx.content_width, + toggle_h, + toggle_col_gap, + toggle_gap, + 1, + item_overrides.len(), + ); + for (item, override_item) in item_layout.items.iter().zip(item_overrides.iter()) { + hits.push(HitRegion { + rect: (item.x, item.y, item.w, item.h), + event: activation_event(&override_item.activation), + kind: HitKind::Click, + tooltip: override_item.tooltip.as_string(), + }); + } } fn activation_event(activation: &ToolbarActivation) -> crate::ui::toolbar::ToolbarEvent { diff --git a/src/backend/wayland/toolbar/layout/side/sliders.rs b/src/backend/wayland/toolbar/layout/side/sliders.rs index dde5586f..c7a47b5d 100644 --- a/src/backend/wayland/toolbar/layout/side/sliders.rs +++ b/src/backend/wayland/toolbar/layout/side/sliders.rs @@ -7,6 +7,13 @@ pub(super) fn push_thickness_hits( y: f64, hits: &mut Vec, ) -> f64 { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::Thickness) + { + return y; + } + let card_h = ctx.spec.side_thickness_height(ctx.snapshot); super::section_header::push_collapsible_header_hit(ctx, y, ToolbarSideSection::Thickness, hits); if ctx @@ -58,6 +65,13 @@ pub(super) fn push_eraser_mode_hits( y: f64, hits: &mut Vec, ) -> f64 { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::EraserMode) + { + return y; + } + let card_h = ctx.spec.side_eraser_mode_height(ctx.snapshot); super::section_header::push_collapsible_header_hit( ctx, @@ -99,6 +113,13 @@ pub(super) fn push_polygon_sides_hits( y: f64, hits: &mut Vec, ) -> f64 { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::PolygonSides) + { + return y; + } + let card_h = ctx.spec.side_polygon_sides_height(ctx.snapshot); super::section_header::push_collapsible_header_hit( ctx, @@ -142,7 +163,11 @@ pub(super) fn push_text_size_hits( y: f64, hits: &mut Vec, ) -> f64 { - if !ToolContext::from_snapshot(ctx.snapshot).show_font_controls { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::TextSize) + || !ToolContext::from_snapshot(ctx.snapshot).show_font_controls + { return y; } @@ -176,7 +201,11 @@ pub(super) fn push_marker_opacity_hits( y: f64, hits: &mut Vec, ) -> f64 { - if !ToolContext::from_snapshot(ctx.snapshot).show_marker_opacity { + if ctx + .snapshot + .side_section_hidden(ToolbarSideSection::MarkerOpacity) + || !ToolContext::from_snapshot(ctx.snapshot).show_marker_opacity + { return y; } @@ -218,7 +247,9 @@ pub(super) fn push_font_hits( y: f64, hits: &mut Vec, ) -> f64 { - if !ToolContext::from_snapshot(ctx.snapshot).show_font_controls { + if ctx.snapshot.side_section_hidden(ToolbarSideSection::Font) + || !ToolContext::from_snapshot(ctx.snapshot).show_font_controls + { return y; } diff --git a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs index 15e61d30..a79d0236 100644 --- a/src/backend/wayland/toolbar/layout/spec/side/sizes.rs +++ b/src/backend/wayland/toolbar/layout/spec/side/sizes.rs @@ -19,14 +19,28 @@ impl ToolbarLayoutSpec { let show_actions = ToolbarActionsModel::from_snapshot(snapshot).is_some(); let show_pages = toolbar_pages_model(snapshot).is_some(); let show_boards = toolbar_boards_model(snapshot).is_some(); - let show_presets = - snapshot.show_presets && snapshot.preset_slot_count.min(snapshot.presets.len()) > 0; + let drawer_session = + snapshot.drawer_open && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Session; + let drawer_customizing = snapshot.drawer_open + && (snapshot.customize_items_open + || snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Customize); + let drawer_sections = + snapshot.drawer_open && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Sections; + let show_presets = !snapshot.side_section_hidden(ToolbarSideSection::Presets) + && snapshot.show_presets + && snapshot.preset_slot_count.min(snapshot.presets.len()) > 0; let show_step_section = snapshot.show_step_section && snapshot.drawer_open - && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::App; - let show_settings_section = snapshot.show_settings_section - && snapshot.drawer_open - && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::App; + && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::App + && !snapshot.side_section_hidden(ToolbarSideSection::StepUndo); + let show_settings_section = if drawer_customizing || drawer_sections { + ToolbarSettingsModel::from_snapshot(snapshot).is_some() + } else { + snapshot.show_settings_section + && snapshot.drawer_open + && snapshot.drawer_tab == crate::input::ToolbarDrawerTab::App + && !snapshot.side_section_hidden(ToolbarSideSection::Settings) + }; let show_session_section = ToolbarSessionModel::from_snapshot(snapshot).is_some(); let mut height: f64 = base_height; @@ -36,7 +50,19 @@ impl ToolbarLayoutSpec { } }; - if tool_context.needs_color { + if drawer_customizing || drawer_session { + add_section(self.side_drawer_tabs_height(snapshot), &mut height); + if drawer_customizing && show_settings_section { + add_section(self.side_settings_height(snapshot), &mut height); + } + if drawer_session && show_session_section { + add_section(self.side_session_height(snapshot), &mut height); + } + height += Self::SIDE_FOOTER_PADDING; + 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); } @@ -46,30 +72,46 @@ impl ToolbarLayoutSpec { } if tool_context.needs_thickness { - add_section(self.side_thickness_height(snapshot), &mut height); - if tool_context.show_eraser_mode { + 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 { + 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 { + 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 { + 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 { + 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 { - add_section(self.side_text_size_height(snapshot), &mut height); - add_section(self.side_font_height(snapshot), &mut height); + 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); + } } if snapshot.drawer_open { @@ -132,6 +174,9 @@ impl ToolbarLayoutSpec { &self, snapshot: &ToolbarSnapshot, ) -> f64 { + if snapshot.side_section_hidden(ToolbarSideSection::Colors) { + return 0.0; + } if snapshot.side_section_collapsed(ToolbarSideSection::Colors) { return Self::SIDE_COLLAPSED_SECTION_HEIGHT; } @@ -329,7 +374,11 @@ impl ToolbarLayoutSpec { if !snapshot.drawer_open { return 0.0; } - Self::SIDE_SECTION_TOGGLE_OFFSET_Y + Self::SIDE_TOGGLE_HEIGHT + Self::SIDE_ACTION_BUTTON_GAP + let rows = crate::input::ToolbarDrawerTab::ALL.len().div_ceil(3); + Self::SIDE_SECTION_TOGGLE_OFFSET_Y + + Self::SIDE_TOGGLE_HEIGHT * rows as f64 + + Self::SIDE_TOGGLE_GAP * rows.saturating_sub(1) as f64 + + Self::SIDE_ACTION_BUTTON_GAP } pub(in crate::backend::wayland::toolbar) fn side_pages_height( @@ -380,6 +429,9 @@ impl ToolbarLayoutSpec { &self, snapshot: &ToolbarSnapshot, ) -> f64 { + if snapshot.side_section_hidden(ToolbarSideSection::StepUndo) { + return 0.0; + } if snapshot.side_section_collapsed(ToolbarSideSection::StepUndo) { return Self::SIDE_COLLAPSED_SECTION_HEIGHT; } @@ -403,7 +455,13 @@ impl ToolbarLayoutSpec { &self, snapshot: &ToolbarSnapshot, ) -> f64 { - if snapshot.side_section_collapsed(ToolbarSideSection::Settings) { + let dedicated_panel = snapshot.customize_items_open + || matches!( + snapshot.drawer_tab, + crate::input::ToolbarDrawerTab::Sections + | crate::input::ToolbarDrawerTab::Customize + ); + if !dedicated_panel && snapshot.side_section_collapsed(ToolbarSideSection::Settings) { return Self::SIDE_COLLAPSED_SECTION_HEIGHT; } let toggle_h = Self::SIDE_TOGGLE_HEIGHT; @@ -418,8 +476,33 @@ impl ToolbarLayoutSpec { } else { 0.0 }; - let buttons_h = Self::SIDE_SETTINGS_BUTTON_HEIGHT; - let content_h = toggle_rows_h + toggle_gap + buttons_h; + let button_rows = settings.buttons().len().div_ceil(2); + let buttons_h = if button_rows > 0 { + Self::SIDE_SETTINGS_BUTTON_HEIGHT * button_rows as f64 + } else { + 0.0 + }; + let group_rows = settings.groups().len().div_ceil(2); + let group_rows_h = if group_rows > 0 { + toggle_h + + toggle_gap + + Self::SIDE_SETTINGS_BUTTON_HEIGHT * group_rows as f64 + + Self::SIDE_SETTINGS_BUTTON_GAP * (group_rows as f64 - 1.0) + } else { + 0.0 + }; + let item_rows = settings.item_overrides().len(); + let item_rows_h = if item_rows > 0 { + toggle_h + + toggle_gap + + toggle_h * item_rows as f64 + + toggle_gap * (item_rows as f64 - 1.0) + } else { + 0.0 + }; + let customize_h = group_rows_h + item_rows_h; + let customize_gap = if customize_h > 0.0 { toggle_gap } else { 0.0 }; + let content_h = toggle_rows_h + toggle_gap + buttons_h + customize_gap + customize_h; Self::SIDE_SECTION_TOGGLE_OFFSET_Y + content_h + Self::SIDE_SETTINGS_BUTTON_GAP } @@ -490,6 +573,9 @@ impl ToolbarLayoutSpec { section: ToolbarSideSection, expanded_height: f64, ) -> f64 { + if snapshot.side_section_hidden(section) { + return 0.0; + } if snapshot.side_section_collapsed(section) { Self::SIDE_COLLAPSED_SECTION_HEIGHT } else { diff --git a/src/backend/wayland/toolbar/layout/spec/top.rs b/src/backend/wayland/toolbar/layout/spec/top.rs index 034dc1cc..219c5ad4 100644 --- a/src/backend/wayland/toolbar/layout/spec/top.rs +++ b/src/backend/wayland/toolbar/layout/spec/top.rs @@ -37,16 +37,25 @@ impl ToolbarLayoutSpec { Self::TOP_SIZE_TEXT.1 }; let mut height = base_height as f64; - if self.shape_picker_open { + if self.shape_picker_open && model::top_shape_picker_visible(snapshot) { let (_, btn_h) = if self.use_icons { (Self::TOP_ICON_BUTTON, Self::TOP_ICON_BUTTON) } else { (Self::TOP_TEXT_BUTTON_W, Self::TOP_TEXT_BUTTON_H) }; let row_count = if self.layout_mode == ToolbarLayoutMode::Simple { - 2.0 - } else { + 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 }; height += row_count * (btn_h + Self::TOP_SHAPE_ROW_GAP); } @@ -57,29 +66,60 @@ impl ToolbarLayoutSpec { } else { Self::TOP_TEXT_BUTTON_W }; - let tool_count = - model::top_tool_buttons(self.layout_mode == ToolbarLayoutMode::Simple).len(); - let mut x = Self::TOP_START_X + Self::TOP_HANDLE_SIZE + gap; + let tool_count = model::visible_top_tool_buttons( + self.layout_mode == ToolbarLayoutMode::Simple, + snapshot, + ) + .count(); + let mut x = Self::TOP_START_X; + if model::toolbar_item_visible(snapshot, "top.chrome.drag") { + x += Self::TOP_HANDLE_SIZE + gap; + } x += tool_count as f64 * (btn_w + gap); - x += btn_w + gap; // Shapes/Polygons picker + if model::top_shape_picker_visible(snapshot) { + x += btn_w + gap; // Shapes/Polygons picker + } let fill_tool_active = model::fill_tool_active(snapshot.active_tool, snapshot.tool_override); - let fill_visible = !self.use_icons && fill_tool_active && !self.shape_picker_open; + let fill_visible = !self.use_icons + && fill_tool_active + && !self.shape_picker_open + && model::top_fill_visible(snapshot); if fill_visible { x += Self::TOP_TEXT_FILL_W + gap; } - x += btn_w + gap; // Text button - x += btn_w + gap; // Note button + 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 { - x += btn_w + gap; // Clear - if self.use_icons { + 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 } } - let left_end = x + Self::TOP_TOGGLE_WIDTH; - let right_controls = Self::TOP_PIN_BUTTON_SIZE * 2.0 - + Self::TOP_PIN_BUTTON_GAP - + Self::TOP_PIN_BUTTON_MARGIN_RIGHT; + let left_end = if model::top_icon_mode_toggle_visible(snapshot) { + x + Self::TOP_TOGGLE_WIDTH + } else { + x + }; + let right_control_count = + usize::from(model::toolbar_item_visible(snapshot, "top.chrome.pin")) + + usize::from(model::toolbar_item_visible(snapshot, "top.chrome.close")); + let right_controls = if right_control_count == 0 { + 0.0 + } else { + Self::TOP_PIN_BUTTON_SIZE * right_control_count as f64 + + 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; (width.ceil() as u32, height.ceil() as u32) @@ -115,15 +155,4 @@ impl ToolbarLayoutSpec { (height - Self::TOP_PIN_BUTTON_SIZE) / 2.0 } } - - pub(in crate::backend::wayland::toolbar) fn top_pin_x(&self, width: f64) -> f64 { - width - - Self::TOP_PIN_BUTTON_SIZE * 2.0 - - Self::TOP_PIN_BUTTON_GAP - - Self::TOP_PIN_BUTTON_MARGIN_RIGHT - } - - pub(in crate::backend::wayland::toolbar) fn top_close_x(&self, width: f64) -> f64 { - width - Self::TOP_PIN_BUTTON_SIZE - Self::TOP_PIN_BUTTON_MARGIN_RIGHT - } } diff --git a/src/backend/wayland/toolbar/layout/tests/collapsible.rs b/src/backend/wayland/toolbar/layout/tests/collapsible.rs index 3c8196fb..b48b4cad 100644 --- a/src/backend/wayland/toolbar/layout/tests/collapsible.rs +++ b/src/backend/wayland/toolbar/layout/tests/collapsible.rs @@ -69,7 +69,7 @@ fn collapsed_header_hit_excludes_body_start_boundary() { fn side_session_collapsed_hides_body_hits_and_keeps_expand_hit() { let mut state = create_test_input_state(); state.toolbar_drawer_open = true; - state.toolbar_drawer_tab = ToolbarDrawerTab::App; + state.toolbar_drawer_tab = ToolbarDrawerTab::Session; let mut expanded = snapshot_from_state(&state); expanded.active_session_path = @@ -174,7 +174,6 @@ fn common_side_sections_collapsed_keep_only_headers() { ToolbarSideSection::Font, ToolbarSideSection::Actions, ToolbarSideSection::StepUndo, - ToolbarSideSection::Session, ToolbarSideSection::Settings, ]; state diff --git a/src/backend/wayland/toolbar/layout/tests/mod.rs b/src/backend/wayland/toolbar/layout/tests/mod.rs index 345a5824..627fc96e 100644 --- a/src/backend/wayland/toolbar/layout/tests/mod.rs +++ b/src/backend/wayland/toolbar/layout/tests/mod.rs @@ -261,7 +261,7 @@ fn side_settings_static_hits_include_model_controls() { fn side_session_static_hits_include_model_controls_and_recents() { let mut state = create_test_input_state(); state.toolbar_drawer_open = true; - state.toolbar_drawer_tab = ToolbarDrawerTab::App; + state.toolbar_drawer_tab = ToolbarDrawerTab::Session; let mut snapshot = snapshot_from_state(&state); snapshot.active_session_path = Some(std::path::PathBuf::from("/tmp/current.wayscriber-session")); @@ -293,7 +293,7 @@ fn side_session_static_hits_include_model_controls_and_recents() { fn side_session_overwrite_confirmation_hits_replace_action_buttons() { let mut state = create_test_input_state(); state.toolbar_drawer_open = true; - state.toolbar_drawer_tab = ToolbarDrawerTab::App; + state.toolbar_drawer_tab = ToolbarDrawerTab::Session; let mut snapshot = snapshot_from_state(&state); let target = std::path::PathBuf::from("/tmp/existing.wayscriber-session"); snapshot.active_session_path = diff --git a/src/backend/wayland/toolbar/layout/top/icons.rs b/src/backend/wayland/toolbar/layout/top/icons.rs index 36ad476f..4562caaf 100644 --- a/src/backend/wayland/toolbar/layout/top/icons.rs +++ b/src/backend/wayland/toolbar/layout/top/icons.rs @@ -3,7 +3,6 @@ use super::super::super::format_binding_label; use super::super::super::hit::HitRegion; use super::super::spec::ToolbarLayoutSpec; use super::shape_buttons; -use super::tool_buttons; use crate::config::{Action, action_label}; use crate::ui::toolbar::bindings::tool_tooltip_label; use crate::ui::toolbar::model; @@ -23,30 +22,29 @@ pub(super) fn build_hits( let (btn_size, _) = spec.top_button_size(); let y = spec.top_button_y(height); let mut fill_anchor: Option<(f64, f64)> = None; - let tool_buttons = tool_buttons(is_simple); let mut rect_x = None; let mut circle_end_x = None; - for tool in tool_buttons { - if model::is_fill_tool(*tool) && rect_x.is_none() { + for tool in model::visible_top_tool_buttons(is_simple, snapshot) { + if model::is_fill_tool(tool) && rect_x.is_none() { rect_x = Some(x); } - if model::is_fill_tool(*tool) { + if model::is_fill_tool(tool) { circle_end_x = Some(x + btn_size); } hits.push(HitRegion { rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::SelectTool(*tool), + event: ToolbarEvent::SelectTool(tool), kind: HitKind::Click, tooltip: Some(format_binding_label( - tool_tooltip_label(*tool), - snapshot.binding_hints.for_tool(*tool), + tool_tooltip_label(tool), + snapshot.binding_hints.for_tool(tool), )), }); x += btn_size + gap; } - if is_simple { + if model::top_shape_picker_visible(snapshot) && is_simple { hits.push(HitRegion { rect: (x, y, btn_size, btn_size), event: ToolbarEvent::ToggleShapePicker(!snapshot.shape_picker_open), @@ -57,7 +55,7 @@ pub(super) fn build_hits( fill_anchor = Some((x, btn_size)); } x += btn_size + gap; - } else { + } else if model::top_shape_picker_visible(snapshot) { let current_shape_tool = model::current_shape_tool(snapshot.active_tool, snapshot.tool_override); let current_polygon_tool = current_shape_tool.filter(|tool| model::is_polygon_tool(*tool)); @@ -77,6 +75,7 @@ pub(super) fn build_hits( if fill_tool_active && !snapshot.shape_picker_open + && model::top_fill_visible(snapshot) && let Some((fill_x, fill_w)) = fill_anchor { let fill_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; @@ -98,99 +97,125 @@ pub(super) fn build_hits( }); } - 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; - - 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_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 !is_simple { + if model::top_sticky_note_visible(snapshot) { hits.push(HitRegion { rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::ClearCanvas, + event: ToolbarEvent::EnterStickyNoteMode, kind: HitKind::Click, tooltip: Some(format_binding_label( - action_label(Action::ClearCanvas), + action_label(Action::EnterStickyNoteMode), snapshot .binding_hints - .binding_for_action(Action::ClearCanvas), + .binding_for_action(Action::EnterStickyNoteMode), )), }); x += btn_size + gap; + } - let highlight_x = x; + if model::top_screenshot_visible(snapshot) { hits.push(HitRegion { rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::ToggleAllHighlight(!snapshot.any_highlight_active), + event: ToolbarEvent::CaptureScreenshot, kind: HitKind::Click, tooltip: Some(format_binding_label( - action_label(Action::ToggleHighlightTool), + action_label(Action::CaptureSelection), snapshot .binding_hints - .binding_for_action(Action::ToggleHighlightTool), + .binding_for_action(Action::CaptureSelection), )), }); - if snapshot.highlight_tool_active { - let ring_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; + x += btn_size + gap; + } + + if !is_simple { + if model::top_clear_canvas_visible(snapshot) { 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; + } + + 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; + 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; } - x += btn_size + gap; } - hits.push(HitRegion { - rect: (x, y, ToolbarLayoutSpec::TOP_TOGGLE_WIDTH, btn_size), - event: ToolbarEvent::ToggleIconMode(false), - kind: HitKind::Click, - tooltip: None, - }); + if model::top_icon_mode_toggle_visible(snapshot) { + hits.push(HitRegion { + rect: (x, y, ToolbarLayoutSpec::TOP_TOGGLE_WIDTH, btn_size), + event: ToolbarEvent::ToggleIconMode(false), + kind: HitKind::Click, + tooltip: None, + }); + } - if snapshot.shape_picker_open { + if snapshot.shape_picker_open && model::top_shape_picker_visible(snapshot) { let mut shape_y = y + btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - push_picker_hits( - shape_y, - btn_size, - gap, - if is_simple { - model::common_shape_tools() - } else { - shape_buttons() - }, - snapshot, - hits, - ); - if is_simple { + 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); shape_y += btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - push_picker_hits(shape_y, btn_size, gap, shape_buttons(), snapshot, hits); + } + 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); + } } } } @@ -205,6 +230,9 @@ fn push_picker_hits( ) { let mut shape_x = ToolbarLayoutSpec::TOP_START_X + ToolbarLayoutSpec::TOP_HANDLE_SIZE + gap; for tool in tools { + if !model::tool_visible(snapshot, *tool) { + continue; + } hits.push(HitRegion { rect: (shape_x, shape_y, btn_size, btn_size), event: ToolbarEvent::SelectTool(*tool), diff --git a/src/backend/wayland/toolbar/layout/top/mod.rs b/src/backend/wayland/toolbar/layout/top/mod.rs index 19988800..0a2e86e3 100644 --- a/src/backend/wayland/toolbar/layout/top/mod.rs +++ b/src/backend/wayland/toolbar/layout/top/mod.rs @@ -30,29 +30,29 @@ pub fn build_top_hits( let btn_size = ToolbarLayoutSpec::TOP_PIN_BUTTON_SIZE; let btn_y = spec.top_pin_button_y(height); - let pin_x = spec.top_pin_x(width); - hits.push(HitRegion { - rect: (pin_x, btn_y, btn_size, btn_size), - event: ToolbarEvent::PinTopToolbar(!snapshot.top_pinned), - kind: HitKind::Click, - tooltip: Some(if snapshot.top_pinned { - "Unpin".to_string() - } else { - "Pin".to_string() - }), - }); - - let close_x = spec.top_close_x(width); - hits.push(HitRegion { - rect: (close_x, btn_y, btn_size, btn_size), - event: ToolbarEvent::CloseTopToolbar, - kind: HitKind::Click, - tooltip: Some("Close".to_string()), - }); -} + let mut right_x = width - ToolbarLayoutSpec::TOP_PIN_BUTTON_MARGIN_RIGHT - btn_size; + if model::toolbar_item_visible(snapshot, "top.chrome.close") { + hits.push(HitRegion { + rect: (right_x, btn_y, btn_size, btn_size), + event: ToolbarEvent::CloseTopToolbar, + kind: HitKind::Click, + tooltip: Some("Close".to_string()), + }); + right_x -= btn_size + ToolbarLayoutSpec::TOP_PIN_BUTTON_GAP; + } -fn tool_buttons(is_simple: bool) -> &'static [Tool] { - model::top_tool_buttons(is_simple) + if model::toolbar_item_visible(snapshot, "top.chrome.pin") { + hits.push(HitRegion { + rect: (right_x, btn_y, btn_size, btn_size), + event: ToolbarEvent::PinTopToolbar(!snapshot.top_pinned), + kind: HitKind::Click, + tooltip: Some(if snapshot.top_pinned { + "Unpin".to_string() + } else { + "Pin".to_string() + }), + }); + } } fn shape_buttons() -> &'static [Tool] { diff --git a/src/backend/wayland/toolbar/layout/top/text.rs b/src/backend/wayland/toolbar/layout/top/text.rs index e0c80fa6..582924d1 100644 --- a/src/backend/wayland/toolbar/layout/top/text.rs +++ b/src/backend/wayland/toolbar/layout/top/text.rs @@ -3,7 +3,6 @@ use super::super::super::format_binding_label; use super::super::super::hit::HitRegion; use super::super::spec::ToolbarLayoutSpec; use super::shape_buttons; -use super::tool_buttons; use crate::config::{Action, action_label}; use crate::ui::toolbar::bindings::tool_tooltip_label; use crate::ui::toolbar::model; @@ -22,23 +21,21 @@ pub(super) fn build_hits( let (btn_w, btn_h) = spec.top_button_size(); let y = spec.top_button_y(height); - let tool_buttons = tool_buttons(is_simple); - - for tool in tool_buttons { - let tooltip_label = tool_tooltip_label(*tool); + for tool in model::visible_top_tool_buttons(is_simple, snapshot) { + let tooltip_label = tool_tooltip_label(tool); hits.push(HitRegion { rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::SelectTool(*tool), + event: ToolbarEvent::SelectTool(tool), kind: HitKind::Click, tooltip: Some(format_binding_label( tooltip_label, - snapshot.binding_hints.for_tool(*tool), + snapshot.binding_hints.for_tool(tool), )), }); x += btn_w + gap; } - if is_simple { + if model::top_shape_picker_visible(snapshot) && is_simple { hits.push(HitRegion { rect: (x, y, btn_w, btn_h), event: ToolbarEvent::ToggleShapePicker(!snapshot.shape_picker_open), @@ -46,7 +43,7 @@ pub(super) fn build_hits( tooltip: Some("Shapes".to_string()), }); x += btn_w + gap; - } else { + } else if model::top_shape_picker_visible(snapshot) { hits.push(HitRegion { rect: (x, y, btn_w, btn_h), event: ToolbarEvent::ToggleShapePicker(!snapshot.shape_picker_open), @@ -56,7 +53,7 @@ pub(super) fn build_hits( x += btn_w + gap; } - if fill_tool_active && !snapshot.shape_picker_open { + if fill_tool_active && !snapshot.shape_picker_open && model::top_fill_visible(snapshot) { let fill_w = ToolbarLayoutSpec::TOP_TEXT_FILL_W; hits.push(HitRegion { rect: (x, y, fill_w, btn_h), @@ -72,33 +69,52 @@ pub(super) fn build_hits( x += fill_w + gap; } - 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_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; + } - 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_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 !is_simple { + 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) { hits.push(HitRegion { rect: (x, y, btn_w, btn_h), event: ToolbarEvent::ClearCanvas, @@ -113,31 +129,31 @@ pub(super) fn build_hits( x += btn_w + gap; } - hits.push(HitRegion { - rect: (x, y, ToolbarLayoutSpec::TOP_TOGGLE_WIDTH, btn_h), - event: ToolbarEvent::ToggleIconMode(true), - kind: HitKind::Click, - tooltip: None, - }); + if model::top_icon_mode_toggle_visible(snapshot) { + hits.push(HitRegion { + rect: (x, y, ToolbarLayoutSpec::TOP_TOGGLE_WIDTH, btn_h), + event: ToolbarEvent::ToggleIconMode(true), + kind: HitKind::Click, + tooltip: None, + }); + } - if snapshot.shape_picker_open { + if snapshot.shape_picker_open && model::top_shape_picker_visible(snapshot) { let mut shape_y = y + btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - push_picker_hits( - shape_y, - btn_w, - btn_h, - gap, - if is_simple { - model::common_shape_tools() - } else { - shape_buttons() - }, - snapshot, - hits, - ); - if is_simple { + 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); shape_y += btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - push_picker_hits(shape_y, btn_w, btn_h, gap, shape_buttons(), snapshot, hits); + } + 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); + } } } } @@ -153,6 +169,9 @@ fn push_picker_hits( ) { let mut shape_x = ToolbarLayoutSpec::TOP_START_X + ToolbarLayoutSpec::TOP_HANDLE_SIZE + gap; for tool in tools { + if !model::tool_visible(snapshot, *tool) { + continue; + } let tooltip_label = tool_tooltip_label(*tool); hits.push(HitRegion { rect: (shape_x, shape_y, btn_w, btn_h), diff --git a/src/backend/wayland/toolbar/render/side_palette/arrow.rs b/src/backend/wayland/toolbar/render/side_palette/arrow.rs index e1f07dbc..9fd3aaab 100644 --- a/src/backend/wayland/toolbar/render/side_palette/arrow.rs +++ b/src/backend/wayland/toolbar/render/side_palette/arrow.rs @@ -33,7 +33,9 @@ pub(super) fn draw_arrow_section(layout: &mut SidePaletteLayout, y: &mut f64) { size: FONT_SIZE_SMALL, }; - if !ToolContext::from_snapshot(snapshot).show_arrow_labels { + if snapshot.side_section_hidden(ToolbarSideSection::ArrowLabels) + || !ToolContext::from_snapshot(snapshot).show_arrow_labels + { return; } diff --git a/src/backend/wayland/toolbar/render/side_palette/colors.rs b/src/backend/wayland/toolbar/render/side_palette/colors.rs index 01eefab4..6718bbe2 100644 --- a/src/backend/wayland/toolbar/render/side_palette/colors.rs +++ b/src/backend/wayland/toolbar/render/side_palette/colors.rs @@ -34,6 +34,10 @@ pub(super) fn draw_colors_section( size: FONT_SIZE_LABEL, }; + if snapshot.side_section_hidden(ToolbarSideSection::Colors) { + return None; + } + let basic_colors: &[ColorSwatch] = &[ (RED, "Red", Some(Action::SetColorRed)), (GREEN, "Green", Some(Action::SetColorGreen)), diff --git a/src/backend/wayland/toolbar/render/side_palette/drawer.rs b/src/backend/wayland/toolbar/render/side_palette/drawer.rs index e8a341e9..8806c1f1 100644 --- a/src/backend/wayland/toolbar/render/side_palette/drawer.rs +++ b/src/backend/wayland/toolbar/render/side_palette/drawer.rs @@ -36,11 +36,15 @@ pub(super) fn draw_drawer_tabs(layout: &mut SidePaletteLayout, y: &mut f64) { let tab_y = *y + ToolbarLayoutSpec::SIDE_SECTION_TOGGLE_OFFSET_Y; let tab_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; let tab_gap = ToolbarLayoutSpec::SIDE_TOGGLE_GAP; - let tab_w = (content_width - tab_gap) / 2.0; - let tabs = [ToolbarDrawerTab::View, ToolbarDrawerTab::App]; + let tabs = ToolbarDrawerTab::ALL; + let tab_columns = 3usize; + let tab_w = (content_width - tab_gap * (tab_columns - 1) as f64) / tab_columns as f64; for (idx, tab) in tabs.iter().enumerate() { - let tab_x = x + (tab_w + tab_gap) * idx as f64; + let tab_col = idx % tab_columns; + let tab_row = idx / tab_columns; + let tab_x = x + (tab_w + tab_gap) * tab_col as f64; + let tab_y = tab_y + (tab_h + tab_gap) * tab_row as f64; let tab_hover = hover .map(|(hx, hy)| point_in_rect(hx, hy, tab_x, tab_y, tab_w, tab_h)) .unwrap_or(false); diff --git a/src/backend/wayland/toolbar/render/side_palette/mod.rs b/src/backend/wayland/toolbar/render/side_palette/mod.rs index 5a87dbf3..992628a6 100644 --- a/src/backend/wayland/toolbar/render/side_palette/mod.rs +++ b/src/backend/wayland/toolbar/render/side_palette/mod.rs @@ -19,8 +19,9 @@ use anyhow::Result; use crate::backend::wayland::toolbar::hit::HitRegion; use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; -use crate::ui::toolbar::ToolbarSnapshot; +use crate::input::ToolbarDrawerTab; use crate::ui::toolbar::snapshot::ToolContext; +use crate::ui::toolbar::{ToolbarSideSection, ToolbarSnapshot}; use std::time::Instant; @@ -93,15 +94,43 @@ pub fn render_side_palette( let mut y = header::draw_header(&mut layout); + if snapshot.drawer_open + && (snapshot.customize_items_open + || snapshot.drawer_tab == ToolbarDrawerTab::Customize + || snapshot.drawer_tab == ToolbarDrawerTab::Session) + { + drawer::draw_drawer_tabs(&mut layout, &mut y); + if snapshot.drawer_tab == ToolbarDrawerTab::Session { + session::draw_session_section(&mut layout, &mut y); + } else { + settings::draw_settings_section(&mut layout, &mut y); + } + draw_tooltip_with_delay( + ctx, + layout.hits, + layout.hover, + width, + height, + false, + hover_start, + ); + return Ok(()); + } + // Color section: only show when the tool needs color - let colors_info = if tool_context.needs_color { - colors::draw_colors_section(&mut layout, &mut y) - } else { - None - }; + 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 = presets::draw_presets_section(&mut layout, &mut y); + let hover_preset_color = if snapshot.side_section_hidden(ToolbarSideSection::Presets) { + None + } else { + presets::draw_presets_section(&mut layout, &mut y) + }; if let (Some(color), Some(info)) = (hover_preset_color, &colors_info) { colors::draw_preset_hover_highlight(&layout, info, color); } @@ -112,22 +141,31 @@ pub fn render_side_palette( } // Arrow labels: show when arrow tool is active - if tool_context.show_arrow_labels { + 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 { + 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 { + 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 { + 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); } diff --git a/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs b/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs index 3563a5f3..a87ce1cb 100644 --- a/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs +++ b/src/backend/wayland/toolbar/render/side_palette/presets/mod.rs @@ -20,7 +20,10 @@ pub(super) fn draw_presets_section(layout: &mut SidePaletteLayout, y: &mut f64) let section_gap = layout.section_gap; let slot_count = snapshot.preset_slot_count.min(snapshot.presets.len()); - if !snapshot.show_presets || slot_count == 0 { + if snapshot.side_section_hidden(ToolbarSideSection::Presets) + || !snapshot.show_presets + || slot_count == 0 + { return None; } diff --git a/src/backend/wayland/toolbar/render/side_palette/settings.rs b/src/backend/wayland/toolbar/render/side_palette/settings.rs index 4f8b965f..31cbdedf 100644 --- a/src/backend/wayland/toolbar/render/side_palette/settings.rs +++ b/src/backend/wayland/toolbar/render/side_palette/settings.rs @@ -3,6 +3,7 @@ use crate::backend::wayland::toolbar::events::HitKind; use crate::backend::wayland::toolbar::hit::HitRegion; 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}; @@ -41,15 +42,25 @@ pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64) let settings_card_h = layout.spec.side_settings_height(snapshot); draw_group_card(ctx, card_x, *y, card_w, settings_card_h); + let customizing = + snapshot.customize_items_open || snapshot.drawer_tab == ToolbarDrawerTab::Customize; + let dedicated_panel = customizing || snapshot.drawer_tab == ToolbarDrawerTab::Sections; + let header_label = if customizing { + "Customize toolbar" + } else if snapshot.drawer_tab == ToolbarDrawerTab::Sections { + "Toolbar sections" + } else { + ToolbarSideSection::Settings.label() + }; draw_collapsible_header( layout, *y, label_style, ToolbarSideSection::Settings, - ToolbarSideSection::Settings.label(), + header_label, ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_TALL, ); - if snapshot.side_section_collapsed(ToolbarSideSection::Settings) { + if !dedicated_panel && snapshot.side_section_collapsed(ToolbarSideSection::Settings) { *y += settings_card_h + section_gap; return; } @@ -149,6 +160,108 @@ pub(super) fn draw_settings_section(layout: &mut SidePaletteLayout, y: &mut f64) }); } + let mut customize_y = buttons_y; + if button_layout.rows > 0 { + customize_y += button_layout.height; + } + customize_y += toggle_gap; + let groups = settings_model.groups(); + if !groups.is_empty() { + draw_label_left( + ctx, + label_style, + x, + customize_y, + content_width, + toggle_h, + "Choose a group", + ); + customize_y += toggle_h + toggle_gap; + } + let group_layout = grid_layout( + x, + customize_y, + button_w, + button_h, + button_gap, + button_gap, + 2, + groups.len(), + ); + for (item, group) in group_layout.items.iter().zip(groups.iter()) { + let group_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) + .unwrap_or(false); + draw_button(ctx, item.x, item.y, item.w, item.h, false, group_hover); + draw_label_center( + ctx, + label_style, + item.x, + item.y, + item.w, + item.h, + group.label.as_ref(), + ); + hits.push(HitRegion { + rect: (item.x, item.y, item.w, item.h), + event: group.event.clone(), + kind: HitKind::Click, + tooltip: group.tooltip.as_string(), + }); + } + + let mut items_y = customize_y; + if group_layout.rows > 0 { + items_y += group_layout.height + toggle_gap; + } + let item_overrides = settings_model.item_overrides(); + if !item_overrides.is_empty() { + draw_label_left( + ctx, + label_style, + x, + items_y, + content_width, + toggle_h, + snapshot + .customize_items_group + .map_or("Uncheck items to hide", |group| group.label()), + ); + items_y += toggle_h + toggle_gap; + } + let item_layout = grid_layout( + x, + items_y, + content_width, + toggle_h, + toggle_col_gap, + toggle_gap, + 1, + item_overrides.len(), + ); + for (item, override_item) in item_layout.items.iter().zip(item_overrides.iter()) { + let item_hover = hover + .map(|(hx, hy)| point_in_rect(hx, hy, item.x, item.y, item.w, item.h)) + .unwrap_or(false); + draw_checkbox( + ctx, + item.x, + item.y, + item.w, + item.h, + override_item.shown, + item_hover, + toggle_style, + override_item.label.as_ref(), + ); + hits.push(HitRegion { + rect: (item.x, item.y, item.w, item.h), + event: activation_event(&override_item.activation), + kind: HitKind::Click, + tooltip: override_item.tooltip.as_string(), + }); + } + *y += settings_card_h + section_gap; } @@ -158,8 +271,19 @@ fn activation_event(activation: &ToolbarActivation) -> crate::ui::toolbar::Toolb fn draw_settings_icon(ctx: &cairo::Context, icon: ToolbarIcon, x: f64, y: f64, size: f64) { match icon { + ToolbarIcon::Back => draw_back_icon(ctx, x, y, size), ToolbarIcon::Settings => toolbar_icons::draw_icon_settings(ctx, x, y, size), + ToolbarIcon::Visibility => toolbar_icons::draw_icon_visibility(ctx, x, y, size), ToolbarIcon::File => toolbar_icons::draw_icon_file(ctx, x, y, size), ToolbarIcon::More | ToolbarIcon::Board => {} } } + +fn draw_back_icon(ctx: &cairo::Context, x: f64, y: f64, size: f64) { + let mid_y = y + size * 0.5; + ctx.set_line_width(2.0); + ctx.move_to(x + size * 0.65, y + size * 0.25); + ctx.line_to(x + size * 0.35, mid_y); + ctx.line_to(x + size * 0.65, y + size * 0.75); + let _ = ctx.stroke(); +} diff --git a/src/backend/wayland/toolbar/render/side_palette/step/mod.rs b/src/backend/wayland/toolbar/render/side_palette/step/mod.rs index e5fcbc2a..d90fcc8c 100644 --- a/src/backend/wayland/toolbar/render/side_palette/step/mod.rs +++ b/src/backend/wayland/toolbar/render/side_palette/step/mod.rs @@ -28,7 +28,8 @@ pub(super) fn draw_step_section(layout: &mut SidePaletteLayout, y: &mut f64) { size: super::super::widgets::constants::FONT_SIZE_LABEL, }; - if !snapshot.show_step_section + if snapshot.side_section_hidden(ToolbarSideSection::StepUndo) + || !snapshot.show_step_section || !snapshot.drawer_open || snapshot.drawer_tab != ToolbarDrawerTab::App { diff --git a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs index 9268c62d..6115a053 100644 --- a/src/backend/wayland/toolbar/render/side_palette/step_marker.rs +++ b/src/backend/wayland/toolbar/render/side_palette/step_marker.rs @@ -33,7 +33,9 @@ pub(super) fn draw_step_marker_section(layout: &mut SidePaletteLayout, y: &mut f size: FONT_SIZE_SMALL, }; - if !ToolContext::from_snapshot(snapshot).show_step_counter { + if snapshot.side_section_hidden(ToolbarSideSection::StepMarkers) + || !ToolContext::from_snapshot(snapshot).show_step_counter + { return; } diff --git a/src/backend/wayland/toolbar/render/side_palette/text.rs b/src/backend/wayland/toolbar/render/side_palette/text.rs index 790316f9..9c1cad64 100644 --- a/src/backend/wayland/toolbar/render/side_palette/text.rs +++ b/src/backend/wayland/toolbar/render/side_palette/text.rs @@ -38,125 +38,135 @@ pub(super) fn draw_text_controls_section(layout: &mut SidePaletteLayout, y: &mut return; } - let slider_card_h = layout.spec.side_text_size_height(snapshot); let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; let nudge_icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; let track_h = ToolbarLayoutSpec::SIDE_TRACK_HEIGHT; let knob_r = ToolbarLayoutSpec::SIDE_TRACK_KNOB_RADIUS; - draw_group_card(ctx, card_x, *y, card_w, slider_card_h); - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::TextSize, - "Text size", - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::TextSize) { - *y += slider_card_h + section_gap; - } else { - let hits = &mut layout.hits; - - let font_size_spec = ToolbarSliderSpec::FONT_SIZE; - let fs_min = font_size_spec.min; - let fs_max = font_size_spec.max; - let fs_step = font_size_spec.step.unwrap_or(2.0); - let fs_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; - - let fs_minus_x = x; - let fs_minus_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, fs_minus_x, fs_slider_row_y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - ctx, - fs_minus_x, - fs_slider_row_y, - btn_size, - btn_size, - false, - fs_minus_hover, - ); - set_icon_color(ctx, fs_minus_hover); - toolbar_icons::draw_icon_minus( - ctx, - fs_minus_x + (btn_size - nudge_icon_size) / 2.0, - fs_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - rect: (fs_minus_x, fs_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::SetFontSize((snapshot.font_size - fs_step).max(fs_min)), - kind: HitKind::Click, - tooltip: Some("Decrease font size".to_string()), - }); - - let fs_plus_x = width - x - btn_size - value_w - 4.0; - let fs_plus_hover = hover - .map(|(hx, hy)| point_in_rect(hx, hy, fs_plus_x, fs_slider_row_y, btn_size, btn_size)) - .unwrap_or(false); - draw_button( - ctx, - fs_plus_x, - fs_slider_row_y, - btn_size, - btn_size, - false, - fs_plus_hover, - ); - set_icon_color(ctx, fs_plus_hover); - toolbar_icons::draw_icon_plus( - ctx, - fs_plus_x + (btn_size - nudge_icon_size) / 2.0, - fs_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - rect: (fs_plus_x, fs_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::SetFontSize((snapshot.font_size + fs_step).min(fs_max)), - kind: HitKind::Click, - tooltip: Some("Increase font size".to_string()), - }); - - let fs_track_x = fs_minus_x + btn_size + SPACING_STD; - let fs_track_w = fs_plus_x - fs_track_x - SPACING_STD; - let fs_track_y = fs_slider_row_y + (btn_size - track_h) / 2.0; - let fs_knob_x = - font_size_spec.knob_center_x(fs_track_x, fs_track_w, knob_r, snapshot.font_size); - - set_color(ctx, COLOR_TRACK_BACKGROUND); - draw_round_rect(ctx, fs_track_x, fs_track_y, fs_track_w, track_h, 4.0); - let _ = ctx.fill(); - set_color(ctx, COLOR_TRACK_KNOB); - ctx.arc( - fs_knob_x, - fs_track_y + track_h / 2.0, - knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); - - hits.push(HitRegion { - rect: (fs_track_x, fs_track_y - 6.0, fs_track_w, track_h + 12.0), - event: ToolbarEvent::SetFontSize(snapshot.font_size), - kind: HitKind::DragSetFontSize, - tooltip: None, - }); - - let fs_text = format!("{:.0}pt", snapshot.font_size); - draw_label_center( - ctx, + if !snapshot.side_section_hidden(ToolbarSideSection::TextSize) { + let slider_card_h = layout.spec.side_text_size_height(snapshot); + draw_group_card(ctx, card_x, *y, card_w, slider_card_h); + draw_collapsible_header( + layout, + *y, label_style, - width - x - value_w, - fs_slider_row_y, - value_w, - btn_size, - &fs_text, + ToolbarSideSection::TextSize, + "Text size", + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, ); + if snapshot.side_section_collapsed(ToolbarSideSection::TextSize) { + *y += slider_card_h + section_gap; + } else { + let hits = &mut layout.hits; + + let font_size_spec = ToolbarSliderSpec::FONT_SIZE; + let fs_min = font_size_spec.min; + let fs_max = font_size_spec.max; + let fs_step = font_size_spec.step.unwrap_or(2.0); + let fs_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; + + let fs_minus_x = x; + let fs_minus_hover = hover + .map(|(hx, hy)| { + point_in_rect(hx, hy, fs_minus_x, fs_slider_row_y, btn_size, btn_size) + }) + .unwrap_or(false); + draw_button( + ctx, + fs_minus_x, + fs_slider_row_y, + btn_size, + btn_size, + false, + fs_minus_hover, + ); + set_icon_color(ctx, fs_minus_hover); + toolbar_icons::draw_icon_minus( + ctx, + fs_minus_x + (btn_size - nudge_icon_size) / 2.0, + fs_slider_row_y + (btn_size - nudge_icon_size) / 2.0, + nudge_icon_size, + ); + hits.push(HitRegion { + rect: (fs_minus_x, fs_slider_row_y, btn_size, btn_size), + event: ToolbarEvent::SetFontSize((snapshot.font_size - fs_step).max(fs_min)), + kind: HitKind::Click, + tooltip: Some("Decrease font size".to_string()), + }); + + let fs_plus_x = width - x - btn_size - value_w - 4.0; + let fs_plus_hover = hover + .map(|(hx, hy)| { + point_in_rect(hx, hy, fs_plus_x, fs_slider_row_y, btn_size, btn_size) + }) + .unwrap_or(false); + draw_button( + ctx, + fs_plus_x, + fs_slider_row_y, + btn_size, + btn_size, + false, + fs_plus_hover, + ); + set_icon_color(ctx, fs_plus_hover); + toolbar_icons::draw_icon_plus( + ctx, + fs_plus_x + (btn_size - nudge_icon_size) / 2.0, + fs_slider_row_y + (btn_size - nudge_icon_size) / 2.0, + nudge_icon_size, + ); + hits.push(HitRegion { + rect: (fs_plus_x, fs_slider_row_y, btn_size, btn_size), + event: ToolbarEvent::SetFontSize((snapshot.font_size + fs_step).min(fs_max)), + kind: HitKind::Click, + tooltip: Some("Increase font size".to_string()), + }); + + let fs_track_x = fs_minus_x + btn_size + SPACING_STD; + let fs_track_w = fs_plus_x - fs_track_x - SPACING_STD; + let fs_track_y = fs_slider_row_y + (btn_size - track_h) / 2.0; + let fs_knob_x = + font_size_spec.knob_center_x(fs_track_x, fs_track_w, knob_r, snapshot.font_size); + + set_color(ctx, COLOR_TRACK_BACKGROUND); + draw_round_rect(ctx, fs_track_x, fs_track_y, fs_track_w, track_h, 4.0); + let _ = ctx.fill(); + set_color(ctx, COLOR_TRACK_KNOB); + ctx.arc( + fs_knob_x, + fs_track_y + track_h / 2.0, + knob_r, + 0.0, + std::f64::consts::PI * 2.0, + ); + let _ = ctx.fill(); + + hits.push(HitRegion { + rect: (fs_track_x, fs_track_y - 6.0, fs_track_w, track_h + 12.0), + event: ToolbarEvent::SetFontSize(snapshot.font_size), + kind: HitKind::DragSetFontSize, + tooltip: None, + }); + + let fs_text = format!("{:.0}pt", snapshot.font_size); + draw_label_center( + ctx, + label_style, + width - x - value_w, + fs_slider_row_y, + value_w, + btn_size, + &fs_text, + ); + + *y += slider_card_h + section_gap; + } + } - *y += slider_card_h + section_gap; + if snapshot.side_section_hidden(ToolbarSideSection::Font) { + return; } let font_card_h = layout.spec.side_font_height(snapshot); diff --git a/src/backend/wayland/toolbar/render/side_palette/thickness.rs b/src/backend/wayland/toolbar/render/side_palette/thickness.rs index 908ba8ea..b59f7b8e 100644 --- a/src/backend/wayland/toolbar/render/side_palette/thickness.rs +++ b/src/backend/wayland/toolbar/render/side_palette/thickness.rs @@ -35,132 +35,136 @@ pub(super) fn draw_thickness_section(layout: &mut SidePaletteLayout, y: &mut f64 }; let tool_context = ToolContext::from_snapshot(snapshot); - let slider_card_h = layout.spec.side_thickness_height(snapshot); - draw_group_card(ctx, card_x, *y, card_w, slider_card_h); - let thickness_label = tool_context.thickness_label; - draw_collapsible_header( - layout, - *y, - label_style, - ToolbarSideSection::Thickness, - thickness_label, - ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, - ); - if snapshot.side_section_collapsed(ToolbarSideSection::Thickness) { - *y += slider_card_h + section_gap; - } else { - let hits = &mut layout.hits; + if !snapshot.side_section_hidden(ToolbarSideSection::Thickness) { + let slider_card_h = layout.spec.side_thickness_height(snapshot); + draw_group_card(ctx, card_x, *y, card_w, slider_card_h); + let thickness_label = tool_context.thickness_label; + draw_collapsible_header( + layout, + *y, + label_style, + ToolbarSideSection::Thickness, + thickness_label, + ToolbarLayoutSpec::SIDE_SECTION_LABEL_OFFSET_Y, + ); + if snapshot.side_section_collapsed(ToolbarSideSection::Thickness) { + *y += slider_card_h + section_gap; + } else { + let hits = &mut layout.hits; - let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; - let nudge_icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; - let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; - let thickness_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; - let track_h = ToolbarLayoutSpec::SIDE_TRACK_HEIGHT; - let knob_r = ToolbarLayoutSpec::SIDE_TRACK_KNOB_RADIUS; - let thickness_spec = ToolbarSliderSpec::THICKNESS; - let nudge_step = thickness_spec.step.unwrap_or(1.0); + let btn_size = ToolbarLayoutSpec::SIDE_NUDGE_SIZE; + let nudge_icon_size = ToolbarLayoutSpec::SIDE_NUDGE_ICON_SIZE; + let value_w = ToolbarLayoutSpec::SIDE_SLIDER_VALUE_WIDTH; + let thickness_slider_row_y = *y + ToolbarLayoutSpec::SIDE_SLIDER_ROW_OFFSET; + let track_h = ToolbarLayoutSpec::SIDE_TRACK_HEIGHT; + let knob_r = ToolbarLayoutSpec::SIDE_TRACK_KNOB_RADIUS; + let thickness_spec = ToolbarSliderSpec::THICKNESS; + let nudge_step = thickness_spec.step.unwrap_or(1.0); - let minus_x = x; - let minus_hover = hover - .map(|(hx, hy)| { - point_in_rect(hx, hy, minus_x, thickness_slider_row_y, btn_size, btn_size) - }) - .unwrap_or(false); - draw_button( - ctx, - minus_x, - thickness_slider_row_y, - btn_size, - btn_size, - false, - minus_hover, - ); - set_icon_color(ctx, minus_hover); - toolbar_icons::draw_icon_minus( - ctx, - minus_x + (btn_size - nudge_icon_size) / 2.0, - thickness_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - rect: (minus_x, thickness_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeThickness(-nudge_step), - kind: HitKind::Click, - tooltip: Some("Decrease thickness".to_string()), - }); + let minus_x = x; + let minus_hover = hover + .map(|(hx, hy)| { + point_in_rect(hx, hy, minus_x, thickness_slider_row_y, btn_size, btn_size) + }) + .unwrap_or(false); + draw_button( + ctx, + minus_x, + thickness_slider_row_y, + btn_size, + btn_size, + false, + minus_hover, + ); + set_icon_color(ctx, minus_hover); + toolbar_icons::draw_icon_minus( + ctx, + minus_x + (btn_size - nudge_icon_size) / 2.0, + thickness_slider_row_y + (btn_size - nudge_icon_size) / 2.0, + nudge_icon_size, + ); + hits.push(HitRegion { + rect: (minus_x, thickness_slider_row_y, btn_size, btn_size), + event: ToolbarEvent::NudgeThickness(-nudge_step), + kind: HitKind::Click, + tooltip: Some("Decrease thickness".to_string()), + }); - let plus_x = width - x - btn_size - value_w - 4.0; - let plus_hover = hover - .map(|(hx, hy)| { - point_in_rect(hx, hy, plus_x, thickness_slider_row_y, btn_size, btn_size) - }) - .unwrap_or(false); - draw_button( - ctx, - plus_x, - thickness_slider_row_y, - btn_size, - btn_size, - false, - plus_hover, - ); - set_icon_color(ctx, plus_hover); - toolbar_icons::draw_icon_plus( - ctx, - plus_x + (btn_size - nudge_icon_size) / 2.0, - thickness_slider_row_y + (btn_size - nudge_icon_size) / 2.0, - nudge_icon_size, - ); - hits.push(HitRegion { - rect: (plus_x, thickness_slider_row_y, btn_size, btn_size), - event: ToolbarEvent::NudgeThickness(nudge_step), - kind: HitKind::Click, - tooltip: Some("Increase thickness".to_string()), - }); + let plus_x = width - x - btn_size - value_w - 4.0; + let plus_hover = hover + .map(|(hx, hy)| { + point_in_rect(hx, hy, plus_x, thickness_slider_row_y, btn_size, btn_size) + }) + .unwrap_or(false); + draw_button( + ctx, + plus_x, + thickness_slider_row_y, + btn_size, + btn_size, + false, + plus_hover, + ); + set_icon_color(ctx, plus_hover); + toolbar_icons::draw_icon_plus( + ctx, + plus_x + (btn_size - nudge_icon_size) / 2.0, + thickness_slider_row_y + (btn_size - nudge_icon_size) / 2.0, + nudge_icon_size, + ); + hits.push(HitRegion { + rect: (plus_x, thickness_slider_row_y, btn_size, btn_size), + event: ToolbarEvent::NudgeThickness(nudge_step), + kind: HitKind::Click, + tooltip: Some("Increase thickness".to_string()), + }); - let track_x = minus_x + btn_size + SPACING_STD; - let track_w = plus_x - track_x - SPACING_STD; - let thickness_track_y = thickness_slider_row_y + (btn_size - track_h) / 2.0; - let knob_x = thickness_spec.knob_center_x(track_x, track_w, knob_r, snapshot.thickness); + let track_x = minus_x + btn_size + SPACING_STD; + let track_w = plus_x - track_x - SPACING_STD; + let thickness_track_y = thickness_slider_row_y + (btn_size - track_h) / 2.0; + let knob_x = thickness_spec.knob_center_x(track_x, track_w, knob_r, snapshot.thickness); - set_color(ctx, COLOR_TRACK_BACKGROUND); - draw_round_rect(ctx, track_x, thickness_track_y, track_w, track_h, 4.0); - let _ = ctx.fill(); - set_color(ctx, COLOR_TRACK_KNOB); - ctx.arc( - knob_x, - thickness_track_y + track_h / 2.0, - knob_r, - 0.0, - std::f64::consts::PI * 2.0, - ); - let _ = ctx.fill(); + set_color(ctx, COLOR_TRACK_BACKGROUND); + draw_round_rect(ctx, track_x, thickness_track_y, track_w, track_h, 4.0); + let _ = ctx.fill(); + set_color(ctx, COLOR_TRACK_KNOB); + ctx.arc( + knob_x, + thickness_track_y + track_h / 2.0, + knob_r, + 0.0, + std::f64::consts::PI * 2.0, + ); + let _ = ctx.fill(); - hits.push(HitRegion { - rect: (track_x, thickness_track_y - 6.0, track_w, track_h + 12.0), - event: ToolbarEvent::SetThickness(snapshot.thickness), - kind: HitKind::DragSetThickness { - min: thickness_spec.min, - max: thickness_spec.max, - }, - tooltip: None, - }); + hits.push(HitRegion { + rect: (track_x, thickness_track_y - 6.0, track_w, track_h + 12.0), + event: ToolbarEvent::SetThickness(snapshot.thickness), + kind: HitKind::DragSetThickness { + min: thickness_spec.min, + max: thickness_spec.max, + }, + tooltip: None, + }); - let thickness_text = format!("{:.0}px", snapshot.thickness); - let value_x = width - x - value_w; - draw_label_center( - ctx, - label_style, - value_x, - thickness_slider_row_y, - value_w, - btn_size, - &thickness_text, - ); - *y += slider_card_h + section_gap; + let thickness_text = format!("{:.0}px", snapshot.thickness); + let value_x = width - x - value_w; + draw_label_center( + ctx, + label_style, + value_x, + thickness_slider_row_y, + value_w, + btn_size, + &thickness_text, + ); + *y += slider_card_h + section_gap; + } } - if tool_context.show_eraser_mode { + if tool_context.show_eraser_mode + && !snapshot.side_section_hidden(ToolbarSideSection::EraserMode) + { let eraser_card_h = layout.spec.side_eraser_mode_height(snapshot); let toggle_h = ToolbarLayoutSpec::SIDE_TOGGLE_HEIGHT; let toggle_w = content_width; @@ -214,7 +218,9 @@ pub(super) fn draw_thickness_section(layout: &mut SidePaletteLayout, y: &mut f64 } } - if tool_context.show_polygon_sides_control { + if tool_context.show_polygon_sides_control + && !snapshot.side_section_hidden(ToolbarSideSection::PolygonSides) + { draw_polygon_sides_section(layout, y, label_style); } } diff --git a/src/backend/wayland/toolbar/render/top_strip/icons/mod.rs b/src/backend/wayland/toolbar/render/top_strip/icons/mod.rs index f511ff41..6a3eae43 100644 --- a/src/backend/wayland/toolbar/render/top_strip/icons/mod.rs +++ b/src/backend/wayland/toolbar/render/top_strip/icons/mod.rs @@ -71,6 +71,7 @@ pub(super) fn draw_icon_strip( if fill_tool_active && !snapshot.shape_picker_open + && model::top_fill_visible(snapshot) && let Some((fill_x, fill_w)) = tool_row.fill_anchor { let fill_y = y + btn_size + ToolbarLayoutSpec::TOP_ICON_FILL_OFFSET; @@ -103,41 +104,43 @@ pub(super) fn draw_icon_strip( x = draw_utility_row(layout, x, y, btn_size, icon_size, gap, is_simple); - let icons_w = ToolbarLayoutSpec::TOP_TOGGLE_WIDTH; - let icons_hover = hover.and_then(|(hx, hy)| { - if point_in_rect(hx, hy, x, y, icons_w, btn_size) { - Some(if hx < x + icons_w / 2.0 { 0 } else { 1 }) - } else { - None - } - }); - let icons_active = if snapshot.use_icons { 0 } else { 1 }; - draw_segmented_control( - ctx, - x, - y, - icons_w, - btn_size, - ("Ico", "Txt"), - icons_active, - icons_hover, - icon_toggle_style, - ); - let half_w = icons_w / 2.0; - layout.hits.push(HitRegion { - rect: (x, y, half_w, btn_size), - event: ToolbarEvent::ToggleIconMode(true), - kind: HitKind::Click, - tooltip: Some("Icons mode".to_string()), - }); - layout.hits.push(HitRegion { - rect: (x + half_w, y, half_w, btn_size), - event: ToolbarEvent::ToggleIconMode(false), - kind: HitKind::Click, - tooltip: Some("Text mode".to_string()), - }); + if model::top_icon_mode_toggle_visible(snapshot) { + let icons_w = ToolbarLayoutSpec::TOP_TOGGLE_WIDTH; + let icons_hover = hover.and_then(|(hx, hy)| { + if point_in_rect(hx, hy, x, y, icons_w, btn_size) { + Some(if hx < x + icons_w / 2.0 { 0 } else { 1 }) + } else { + None + } + }); + let icons_active = if snapshot.use_icons { 0 } else { 1 }; + draw_segmented_control( + ctx, + x, + y, + icons_w, + btn_size, + ("Ico", "Txt"), + icons_active, + icons_hover, + icon_toggle_style, + ); + let half_w = icons_w / 2.0; + layout.hits.push(HitRegion { + rect: (x, y, half_w, btn_size), + event: ToolbarEvent::ToggleIconMode(true), + kind: HitKind::Click, + tooltip: Some("Icons mode".to_string()), + }); + layout.hits.push(HitRegion { + rect: (x + half_w, y, half_w, btn_size), + event: ToolbarEvent::ToggleIconMode(false), + kind: HitKind::Click, + tooltip: Some("Text mode".to_string()), + }); + } - if snapshot.shape_picker_open { + if snapshot.shape_picker_open && model::top_shape_picker_visible(snapshot) { draw_shape_picker_row(layout, handle_w, y, btn_size, icon_size, is_simple); } } 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 3cbd9c8e..7941c292 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 @@ -18,28 +18,24 @@ pub(super) fn draw_shape_picker_row( is_simple: bool, ) { let mut shape_y = y + btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - draw_picker_row( - layout, - handle_w, - shape_y, - btn_size, - icon_size, - if is_simple { - model::common_shape_tools() - } else { - model::polygon_tools() - }, - ); - if is_simple { + if !model::top_shape_picker_visible(layout.snapshot) { + 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); shape_y += btn_size + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - draw_picker_row( - layout, - handle_w, - shape_y, - btn_size, - icon_size, - model::polygon_tools(), - ); + } + 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); + } } } @@ -53,6 +49,9 @@ fn draw_picker_row( ) { let mut shape_x = ToolbarLayoutSpec::TOP_START_X + handle_w + layout.gap; for tool in tools { + if !model::tool_visible(layout.snapshot, *tool) { + continue; + } let is_active = layout.snapshot.active_tool == *tool || layout.snapshot.tool_override == Some(*tool); let is_hover = layout diff --git a/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs b/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs index d4fce6cf..667b4f77 100644 --- a/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs +++ b/src/backend/wayland/toolbar/render/top_strip/icons/tool_row.rs @@ -32,15 +32,15 @@ pub(super) fn draw_tool_row( let mut fill_anchor: Option<(f64, f64)> = None; let mut rect_x = None; let mut circle_end_x = None; - for tool in model::top_tool_buttons(is_simple) { - if model::is_fill_tool(*tool) && rect_x.is_none() { + for tool in model::visible_top_tool_buttons(is_simple, snapshot) { + if model::is_fill_tool(tool) && rect_x.is_none() { rect_x = Some(x); } - if model::is_fill_tool(*tool) { + if model::is_fill_tool(tool) { circle_end_x = Some(x + btn_size); } - let is_active = snapshot.active_tool == *tool || snapshot.tool_override == Some(*tool); + let is_active = snapshot.active_tool == tool || snapshot.tool_override == Some(tool); let is_hover = layout .hover .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_size, btn_size)) @@ -52,23 +52,23 @@ pub(super) fn draw_tool_row( let icon_y = y + (btn_size - icon_size) / 2.0; draw_semantic_tool_icon( layout.ctx, - model::semantic_icon_for_tool(*tool), + model::semantic_icon_for_tool(tool), icon_x, icon_y, icon_size, ); - let tooltip = layout.tool_tooltip(*tool, tool_tooltip_label(*tool)); + let tooltip = layout.tool_tooltip(tool, tool_tooltip_label(tool)); layout.hits.push(HitRegion { rect: (x, y, btn_size, btn_size), - event: ToolbarEvent::SelectTool(*tool), + event: ToolbarEvent::SelectTool(tool), kind: HitKind::Click, tooltip: Some(tooltip), }); x += btn_size + gap; } - if is_simple { + if model::top_shape_picker_visible(snapshot) && is_simple { let shapes_active = snapshot.shape_picker_open || current_shape_tool.is_some(); let shapes_hover = layout .hover @@ -101,7 +101,7 @@ pub(super) fn draw_tool_row( }); fill_anchor = Some((x, btn_size)); x += btn_size + gap; - } else { + } else if model::top_shape_picker_visible(snapshot) { let current_polygon_tool = current_shape_tool.filter(|tool| model::is_polygon_tool(*tool)); let polygons_active = snapshot.shape_picker_open || current_polygon_tool.is_some(); let polygons_hover = layout 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 f816c449..5600472c 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 @@ -5,6 +5,7 @@ use crate::backend::wayland::toolbar::layout::ToolbarLayoutSpec; use crate::config::{Action, action_label}; use crate::toolbar_icons; use crate::ui::toolbar::ToolbarEvent; +use crate::ui::toolbar::model; use crate::ui_text::UiTextStyle; use super::super::super::widgets::constants::{FONT_FAMILY_DEFAULT, FONT_SIZE_SMALL}; @@ -29,71 +30,75 @@ pub(super) fn draw_utility_row( size: FONT_SIZE_SMALL, }; - 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_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; + } - 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 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; + } - if !is_simple { + 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); @@ -117,7 +122,43 @@ pub(super) fn draw_utility_row( )), }); 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; + } + 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); @@ -148,7 +189,7 @@ pub(super) fn draw_utility_row( .binding_for_action(Action::ToggleHighlightTool), )), }); - if snapshot.highlight_tool_active { + 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 diff --git a/src/backend/wayland/toolbar/render/top_strip/mod.rs b/src/backend/wayland/toolbar/render/top_strip/mod.rs index a8b3cdbf..42c3c4e8 100644 --- a/src/backend/wayland/toolbar/render/top_strip/mod.rs +++ b/src/backend/wayland/toolbar/render/top_strip/mod.rs @@ -81,18 +81,22 @@ 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_hover = layout - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, x, handle_y, handle_w, handle_h)) - .unwrap_or(false); - draw_drag_handle(ctx, x, handle_y, handle_w, handle_h, handle_hover); - layout.hits.push(HitRegion { - rect: (x, handle_y, handle_w, handle_h), - event: ToolbarEvent::MoveTopToolbar { x: 0.0, y: 0.0 }, - kind: HitKind::DragMoveTop, - tooltip: Some("Drag toolbar".to_string()), - }); - x += handle_w + layout.gap; + let handle_visible = model::toolbar_item_visible(snapshot, "top.chrome.drag"); + if handle_visible { + let handle_hover = layout + .hover + .map(|(hx, hy)| point_in_rect(hx, hy, x, handle_y, handle_w, handle_h)) + .unwrap_or(false); + draw_drag_handle(ctx, x, handle_y, handle_w, handle_h, handle_hover); + layout.hits.push(HitRegion { + rect: (x, handle_y, handle_w, handle_h), + event: ToolbarEvent::MoveTopToolbar { x: 0.0, y: 0.0 }, + kind: HitKind::DragMoveTop, + tooltip: Some("Drag toolbar".to_string()), + }); + x += handle_w + layout.gap; + } + let picker_handle_w = if handle_visible { handle_w } else { 0.0 }; let is_simple = snapshot.layout_mode == crate::config::ToolbarLayoutMode::Simple; let current_shape_tool = @@ -103,7 +107,7 @@ pub fn render_top_strip( icons::draw_icon_strip( &mut layout, x, - handle_w, + picker_handle_w, is_simple, current_shape_tool, fill_tool_active, @@ -112,7 +116,7 @@ pub fn render_top_strip( text::draw_text_strip( &mut layout, x, - handle_w, + picker_handle_w, is_simple, current_shape_tool, fill_tool_active, @@ -121,35 +125,46 @@ pub fn render_top_strip( let btn_size = ToolbarLayoutSpec::TOP_PIN_BUTTON_SIZE; let btn_y = layout.spec.top_pin_button_y(height); - let pin_x = layout.spec.top_pin_x(width); - let pin_hover = layout - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, pin_x, btn_y, btn_size, btn_size)) - .unwrap_or(false); - draw_pin_button(ctx, pin_x, btn_y, btn_size, snapshot.top_pinned, pin_hover); - layout.hits.push(HitRegion { - rect: (pin_x, btn_y, btn_size, btn_size), - event: ToolbarEvent::PinTopToolbar(!snapshot.top_pinned), - kind: HitKind::Click, - tooltip: Some(if snapshot.top_pinned { - "Pinned: opens at startup (click to disable)".to_string() - } else { - "Pin: click to open at startup".to_string() - }), - }); - - let close_x = layout.spec.top_close_x(width); - let close_hover = layout - .hover - .map(|(hx, hy)| point_in_rect(hx, hy, close_x, btn_y, btn_size, btn_size)) - .unwrap_or(false); - draw_close_button(ctx, close_x, btn_y, btn_size, close_hover); - layout.hits.push(HitRegion { - rect: (close_x, btn_y, btn_size, btn_size), - event: ToolbarEvent::CloseTopToolbar, - kind: HitKind::Click, - tooltip: Some("Close".to_string()), - }); + let mut right_x = width - ToolbarLayoutSpec::TOP_PIN_BUTTON_MARGIN_RIGHT - btn_size; + if model::toolbar_item_visible(snapshot, "top.chrome.close") { + let close_hover = layout + .hover + .map(|(hx, hy)| point_in_rect(hx, hy, right_x, btn_y, btn_size, btn_size)) + .unwrap_or(false); + draw_close_button(ctx, right_x, btn_y, btn_size, close_hover); + layout.hits.push(HitRegion { + rect: (right_x, btn_y, btn_size, btn_size), + event: ToolbarEvent::CloseTopToolbar, + kind: HitKind::Click, + tooltip: Some("Close".to_string()), + }); + right_x -= btn_size + ToolbarLayoutSpec::TOP_PIN_BUTTON_GAP; + } + + if model::toolbar_item_visible(snapshot, "top.chrome.pin") { + let pin_hover = layout + .hover + .map(|(hx, hy)| point_in_rect(hx, hy, right_x, btn_y, btn_size, btn_size)) + .unwrap_or(false); + draw_pin_button( + ctx, + right_x, + btn_y, + btn_size, + snapshot.top_pinned, + pin_hover, + ); + layout.hits.push(HitRegion { + rect: (right_x, btn_y, btn_size, btn_size), + event: ToolbarEvent::PinTopToolbar(!snapshot.top_pinned), + kind: HitKind::Click, + tooltip: Some(if snapshot.top_pinned { + "Pinned: opens at startup (click to disable)".to_string() + } else { + "Pin: click to open at startup".to_string() + }), + }); + } draw_tooltip_with_delay( ctx, diff --git a/src/backend/wayland/toolbar/render/top_strip/text.rs b/src/backend/wayland/toolbar/render/top_strip/text.rs index d76bcfab..c39206d5 100644 --- a/src/backend/wayland/toolbar/render/top_strip/text.rs +++ b/src/backend/wayland/toolbar/render/top_strip/text.rs @@ -40,28 +40,26 @@ pub(super) fn draw_text_strip( size: ICON_TOGGLE_FONT_SIZE, }; - let tool_buttons = model::top_tool_buttons(is_simple); - - for tool in tool_buttons { - let label = tool_label(*tool); - let tooltip_label = tool_tooltip_label(*tool); - let is_active = snapshot.active_tool == *tool || snapshot.tool_override == Some(*tool); + for tool in model::visible_top_tool_buttons(is_simple, snapshot) { + let label = tool_label(tool); + let tooltip_label = tool_tooltip_label(tool); + let is_active = snapshot.active_tool == tool || snapshot.tool_override == Some(tool); 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, is_active, is_hover); draw_label_center(ctx, label_style, x, y, btn_w, btn_h, label); - let tooltip = layout.tool_tooltip(*tool, tooltip_label); + let tooltip = layout.tool_tooltip(tool, tooltip_label); layout.hits.push(HitRegion { rect: (x, y, btn_w, btn_h), - event: ToolbarEvent::SelectTool(*tool), + event: ToolbarEvent::SelectTool(tool), kind: HitKind::Click, tooltip: Some(tooltip), }); x += btn_w + gap; } - if is_simple { + if model::top_shape_picker_visible(snapshot) && is_simple { let shapes_active = snapshot.shape_picker_open || current_shape_tool.is_some(); let shapes_hover = hover .map(|(hx, hy)| point_in_rect(hx, hy, x, y, btn_w, btn_h)) @@ -75,7 +73,7 @@ pub(super) fn draw_text_strip( tooltip: Some("Shapes".to_string()), }); x += btn_w + gap; - } else { + } else if model::top_shape_picker_visible(snapshot) { let current_polygon_tool = current_shape_tool.filter(|tool| model::is_polygon_tool(*tool)); let polygons_active = snapshot.shape_picker_open || current_polygon_tool.is_some(); let polygons_hover = hover @@ -92,7 +90,7 @@ pub(super) fn draw_text_strip( x += btn_w + gap; } - if fill_tool_active && !snapshot.shape_picker_open { + if fill_tool_active && !snapshot.shape_picker_open && model::top_fill_visible(snapshot) { let fill_w = ToolbarLayoutSpec::TOP_TEXT_FILL_W; let fill_hover = hover .map(|(hx, hy)| point_in_rect(hx, hy, x, y, fill_w, btn_h)) @@ -123,59 +121,83 @@ pub(super) fn draw_text_strip( x += fill_w + gap; } - 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_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; + } - 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 { + 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); @@ -203,57 +225,50 @@ pub(super) fn draw_text_strip( x += btn_w + gap; } - let icons_w = ToolbarLayoutSpec::TOP_TOGGLE_WIDTH; - let icons_hover = hover.and_then(|(hx, hy)| { - if point_in_rect(hx, hy, x, y, icons_w, btn_h) { - Some(if hx < x + icons_w / 2.0 { 0 } else { 1 }) - } else { - None - } - }); - let icons_active = if snapshot.use_icons { 0 } else { 1 }; - draw_segmented_control( - ctx, - x, - y, - icons_w, - btn_h, - ("Ico", "Txt"), - icons_active, - icons_hover, - icon_toggle_style, - ); - let half_w = icons_w / 2.0; - layout.hits.push(HitRegion { - rect: (x, y, half_w, btn_h), - event: ToolbarEvent::ToggleIconMode(true), - kind: HitKind::Click, - tooltip: Some("Icons mode".to_string()), - }); - layout.hits.push(HitRegion { - rect: (x + half_w, y, half_w, btn_h), - event: ToolbarEvent::ToggleIconMode(false), - kind: HitKind::Click, - tooltip: Some("Text mode".to_string()), - }); - - if snapshot.shape_picker_open { - let mut shape_y = y + btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; - draw_picker_text_row( - layout, - handle_w, - shape_y, - btn_w, - btn_h, - label_style, - if is_simple { - model::common_shape_tools() + if model::top_icon_mode_toggle_visible(snapshot) { + let icons_w = ToolbarLayoutSpec::TOP_TOGGLE_WIDTH; + let icons_hover = hover.and_then(|(hx, hy)| { + if point_in_rect(hx, hy, x, y, icons_w, btn_h) { + Some(if hx < x + icons_w / 2.0 { 0 } else { 1 }) } else { - model::polygon_tools() - }, + None + } + }); + let icons_active = if snapshot.use_icons { 0 } else { 1 }; + draw_segmented_control( + ctx, + x, + y, + icons_w, + btn_h, + ("Ico", "Txt"), + icons_active, + icons_hover, + icon_toggle_style, ); - if is_simple { - shape_y += btn_h + ToolbarLayoutSpec::TOP_SHAPE_ROW_GAP; + let half_w = icons_w / 2.0; + layout.hits.push(HitRegion { + rect: (x, y, half_w, btn_h), + event: ToolbarEvent::ToggleIconMode(true), + kind: HitKind::Click, + tooltip: Some("Icons mode".to_string()), + }); + layout.hits.push(HitRegion { + rect: (x + half_w, y, half_w, btn_h), + event: ToolbarEvent::ToggleIconMode(false), + kind: HitKind::Click, + tooltip: Some("Text mode".to_string()), + }); + } + + 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, @@ -261,8 +276,23 @@ pub(super) fn draw_text_strip( btn_w, btn_h, label_style, - model::polygon_tools(), + first_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, + ); + } } } } @@ -282,6 +312,9 @@ fn draw_picker_text_row( let snapshot = layout.snapshot; let mut shape_x = ToolbarLayoutSpec::TOP_START_X + handle_w + gap; for tool in tools { + if !model::tool_visible(snapshot, *tool) { + continue; + } let label = tool_label(*tool); let tooltip_label = tool_tooltip_label(*tool); let is_active = snapshot.active_tool == *tool || snapshot.tool_override == Some(*tool); diff --git a/src/config/mod.rs b/src/config/mod.rs index 3c663a33..cb6c56e1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -44,9 +44,10 @@ pub use types::{ PdfOrientation, PdfPageSize, PdfTransparentBackground, PerformanceConfig, PresenterModeConfig, PresenterToolBehavior, PresetSlotsConfig, PresetToolSettingConfig, PresetToolStatesConfig, RenderColorMappingConfig, RenderProfileConfig, RenderProfileExportMode, RenderProfilesConfig, - SessionCompression, SessionConfig, SessionStorageMode, StatusBarStyle, ToolPresetConfig, - ToolbarConfig, ToolbarLayoutMode, ToolbarModeOverride, ToolbarModeOverrides, UiConfig, - validate_pdf_label_template, + ResolvedToolbarItems, SessionCompression, SessionConfig, SessionStorageMode, StatusBarStyle, + ToolPresetConfig, ToolbarConfig, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, + ToolbarItemId, ToolbarItemSurface, ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverride, + ToolbarModeOverrides, UiConfig, toolbar_item_definitions, validate_pdf_label_template, }; #[cfg(tablet)] #[allow(unused_imports)] diff --git a/src/config/types/mod.rs b/src/config/types/mod.rs index f99393c0..d682ffb1 100644 --- a/src/config/types/mod.rs +++ b/src/config/types/mod.rs @@ -50,7 +50,9 @@ pub use status_bar::StatusBarStyle; pub use tablet::{StylusButtonBinding, TabletInputConfig}; #[allow(unused_imports)] pub use toolbar::{ - ToolbarConfig, ToolbarLayoutMode, ToolbarModeOverride, ToolbarModeOverrides, - ToolbarSectionDefaults, + ResolvedToolbarItems, ToolbarConfig, ToolbarGroupId, ToolbarItemCategory, + ToolbarItemDefinition, ToolbarItemId, ToolbarItemSurface, ToolbarItemsConfig, + ToolbarLayoutMode, ToolbarModeOverride, ToolbarModeOverrides, ToolbarSectionDefaults, + toolbar_item_definitions, }; pub use ui::UiConfig; diff --git a/src/config/types/toolbar/config.rs b/src/config/types/toolbar/config.rs index 3ee66e99..eab2ee2f 100644 --- a/src/config/types/toolbar/config.rs +++ b/src/config/types/toolbar/config.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use super::{ToolbarLayoutMode, ToolbarModeOverrides}; +use super::{ToolbarItemsConfig, ToolbarLayoutMode, ToolbarModeOverrides}; /// Toolbar visibility and pinning configuration. /// @@ -17,6 +17,10 @@ pub struct ToolbarConfig { #[serde(default)] pub mode_overrides: ToolbarModeOverrides, + /// Optional item-level toolbar visibility customizations. + #[serde(default)] + pub items: ToolbarItemsConfig, + /// Show the top toolbar (tool selection) on startup #[serde(default = "default_toolbar_top_pinned")] pub top_pinned: bool, @@ -119,6 +123,7 @@ impl Default for ToolbarConfig { Self { layout_mode: default_toolbar_layout_mode(), mode_overrides: ToolbarModeOverrides::default(), + items: ToolbarItemsConfig::default(), top_pinned: default_toolbar_top_pinned(), side_pinned: default_toolbar_side_pinned(), use_icons: default_toolbar_use_icons(), diff --git a/src/config/types/toolbar/items.rs b/src/config/types/toolbar/items.rs new file mode 100644 index 00000000..d545331f --- /dev/null +++ b/src/config/types/toolbar/items.rs @@ -0,0 +1,962 @@ +use std::collections::BTreeSet; +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +/// User-authored item-level toolbar customization. +/// +/// The raw strings are intentionally preserved so unknown IDs from future +/// versions survive unrelated toolbar saves. +#[cfg_attr(feature = "config-schema", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ToolbarItemsConfig { + #[serde(default)] + pub hidden: Vec, +} + +const DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS: &[&str] = &["top.utility.screenshot"]; + +impl Default for ToolbarItemsConfig { + fn default() -> Self { + Self { + hidden: DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS + .iter() + .map(|id| (*id).to_string()) + .collect(), + } + } +} + +impl ToolbarItemsConfig { + pub fn resolved(&self) -> ResolvedToolbarItems { + let mut hidden = BTreeSet::new(); + let mut unknown_hidden = Vec::new(); + + for raw in &self.hidden { + match raw.parse::() { + Ok(id) => { + hidden.insert(id); + } + Err(_) => unknown_hidden.push(raw.clone()), + } + } + + ResolvedToolbarItems { + hidden, + unknown_hidden, + } + } + + #[allow(dead_code)] + pub fn set_hidden(&mut self, id: ToolbarItemId, hidden: bool) { + let mut next = Vec::with_capacity(self.hidden.len() + usize::from(hidden)); + let mut seen_known = BTreeSet::new(); + + for raw in self.hidden.drain(..) { + match raw.parse::() { + Ok(existing) if existing == id => {} + Ok(existing) => { + if seen_known.insert(existing) { + next.push(existing.as_str().to_string()); + } + } + Err(_) => next.push(raw), + } + } + + if hidden { + next.push(id.as_str().to_string()); + } + + self.hidden = next; + } + + pub fn reset_known_hidden_to_defaults(&mut self) -> bool { + let original = self.hidden.clone(); + let mut next: Vec = DEFAULT_HIDDEN_TOOLBAR_ITEM_IDS + .iter() + .map(|id| (*id).to_string()) + .collect(); + + for raw in self.hidden.drain(..) { + if raw.parse::().is_err() { + next.push(raw); + } + } + + let changed = next != original; + self.hidden = next; + changed + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ResolvedToolbarItems { + pub hidden: BTreeSet, + pub unknown_hidden: Vec, +} + +impl ResolvedToolbarItems { + pub fn is_hidden(&self, id: ToolbarItemId) -> bool { + self.hidden.contains(&id) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ToolbarItemId(&'static str); + +impl ToolbarItemId { + pub(crate) const fn from_known(value: &'static str) -> Self { + Self(value) + } + + pub const fn as_str(self) -> &'static str { + self.0 + } +} + +impl fmt::Display for ToolbarItemId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.0) + } +} + +impl FromStr for ToolbarItemId { + type Err = UnknownToolbarItemId; + + fn from_str(value: &str) -> Result { + let normalized = value.trim(); + toolbar_item_definitions() + .iter() + .find(|definition| definition.id.as_str() == normalized) + .map(|definition| definition.id) + .ok_or(UnknownToolbarItemId) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UnknownToolbarItemId; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ToolbarItemDefinition { + pub id: ToolbarItemId, + pub label: &'static str, + pub surface: ToolbarItemSurface, + pub category: ToolbarItemCategory, + pub group: Option, +} + +impl ToolbarItemDefinition { + const fn new( + id: &'static str, + label: &'static str, + surface: ToolbarItemSurface, + category: ToolbarItemCategory, + group: Option, + ) -> Self { + Self { + id: ToolbarItemId::from_known(id), + label, + surface, + category, + group, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ToolbarItemSurface { + Top, + Side, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ToolbarItemCategory { + Chrome, + Tool, + Utility, + Group, + Action, + Page, + Board, + Setting, + Session, + ToolOption, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ToolbarGroupId { + Colors, + Thickness, + EraserMode, + PolygonSides, + ArrowLabels, + StepMarkers, + StepUndo, + MarkerOpacity, + TextSize, + Font, + Actions, + Pages, + Boards, + Presets, + Settings, + Session, +} + +impl ToolbarGroupId { + pub const fn as_str(self) -> &'static str { + match self { + Self::Colors => "colors", + Self::Thickness => "thickness", + Self::EraserMode => "eraser-mode", + Self::PolygonSides => "polygon-sides", + Self::ArrowLabels => "arrow-labels", + Self::StepMarkers => "step-markers", + Self::StepUndo => "step-undo", + Self::MarkerOpacity => "marker-opacity", + Self::TextSize => "text-size", + Self::Font => "font", + Self::Actions => "actions", + Self::Pages => "pages", + Self::Boards => "boards", + Self::Presets => "presets", + Self::Settings => "settings", + Self::Session => "session", + } + } + + 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", + }) + } +} + +impl fmt::Display for ToolbarGroupId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for ToolbarGroupId { + type Err = UnknownToolbarGroupId; + + fn from_str(value: &str) -> Result { + match value.trim() { + "colors" => Ok(Self::Colors), + "thickness" => Ok(Self::Thickness), + "eraser-mode" => Ok(Self::EraserMode), + "polygon-sides" => Ok(Self::PolygonSides), + "arrow-labels" => Ok(Self::ArrowLabels), + "step-markers" => Ok(Self::StepMarkers), + "step-undo" => Ok(Self::StepUndo), + "marker-opacity" => Ok(Self::MarkerOpacity), + "text-size" => Ok(Self::TextSize), + "font" => Ok(Self::Font), + "actions" => Ok(Self::Actions), + "pages" => Ok(Self::Pages), + "boards" => Ok(Self::Boards), + "presets" => Ok(Self::Presets), + "settings" => Ok(Self::Settings), + "session" => Ok(Self::Session), + _ => Err(UnknownToolbarGroupId), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UnknownToolbarGroupId; + +pub fn toolbar_item_definitions() -> &'static [ToolbarItemDefinition] { + TOOLBAR_ITEM_DEFINITIONS +} + +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", + "Regular polygon", + Top, + Tool, + None, + ), + item( + "top.tool.freeform-polygon", + "Freeform polygon", + Top, + Tool, + None, + ), + item( + "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( + "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", + "Highlight ring", + Top, + Utility, + None, + ), + item( + "top.utility.icon-mode-icons", + "Use icons", + Top, + Utility, + None, + ), + item( + "top.utility.icon-mode-text", + "Use text labels", + Top, + Utility, + None, + ), + item( + "side.group.colors", + "Colors", + Side, + Group, + Some(ToolbarGroupId::Colors), + ), + item( + "side.group.thickness", + "Thickness", + Side, + Group, + Some(ToolbarGroupId::Thickness), + ), + item( + "side.group.eraser-mode", + "Eraser mode", + Side, + Group, + Some(ToolbarGroupId::EraserMode), + ), + item( + "side.group.polygon-sides", + "Polygon sides", + Side, + Group, + Some(ToolbarGroupId::PolygonSides), + ), + item( + "side.group.arrow-labels", + "Arrow labels", + Side, + Group, + Some(ToolbarGroupId::ArrowLabels), + ), + item( + "side.group.step-markers", + "Step markers", + Side, + Group, + Some(ToolbarGroupId::StepMarkers), + ), + item( + "side.group.step-undo", + "Step Undo/Redo", + Side, + Group, + Some(ToolbarGroupId::StepUndo), + ), + item( + "side.group.marker-opacity", + "Marker opacity", + Side, + Group, + Some(ToolbarGroupId::MarkerOpacity), + ), + item( + "side.group.text-size", + "Text size", + Side, + Group, + Some(ToolbarGroupId::TextSize), + ), + item( + "side.group.font", + "Font", + Side, + Group, + Some(ToolbarGroupId::Font), + ), + item( + "side.group.actions", + "Actions", + Side, + Group, + Some(ToolbarGroupId::Actions), + ), + item( + "side.group.pages", + "Pages", + Side, + Group, + Some(ToolbarGroupId::Pages), + ), + item( + "side.group.boards", + "Boards", + Side, + Group, + Some(ToolbarGroupId::Boards), + ), + item( + "side.group.presets", + "Presets", + Side, + Group, + Some(ToolbarGroupId::Presets), + ), + item( + "side.group.settings", + "Settings", + Side, + Group, + Some(ToolbarGroupId::Settings), + ), + item( + "side.group.session", + "Session", + Side, + Group, + Some(ToolbarGroupId::Session), + ), + item( + "side.actions.undo", + "Undo", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.redo", + "Redo", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.clear-canvas", + "Clear canvas", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.zoom-in", + "Zoom in", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.zoom-out", + "Zoom out", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.reset-zoom", + "Reset zoom", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.toggle-zoom-lock", + "Toggle zoom lock", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.undo-all", + "Undo all", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.redo-all", + "Redo all", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.undo-all-delayed", + "Delayed undo all", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.redo-all-delayed", + "Delayed redo all", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.actions.freeze", + "Freeze", + Side, + Action, + Some(ToolbarGroupId::Actions), + ), + item( + "side.pages.previous", + "Previous page", + Side, + Page, + Some(ToolbarGroupId::Pages), + ), + item( + "side.pages.next", + "Next page", + Side, + Page, + Some(ToolbarGroupId::Pages), + ), + item( + "side.pages.new", + "New page", + Side, + Page, + Some(ToolbarGroupId::Pages), + ), + item( + "side.pages.duplicate", + "Duplicate page", + Side, + Page, + Some(ToolbarGroupId::Pages), + ), + item( + "side.pages.delete", + "Delete page", + Side, + Page, + Some(ToolbarGroupId::Pages), + ), + item( + "side.boards.previous", + "Previous board", + Side, + Board, + Some(ToolbarGroupId::Boards), + ), + item( + "side.boards.next", + "Next board", + Side, + Board, + Some(ToolbarGroupId::Boards), + ), + item( + "side.boards.new", + "New board", + Side, + Board, + Some(ToolbarGroupId::Boards), + ), + item( + "side.boards.duplicate", + "Duplicate board", + Side, + Board, + Some(ToolbarGroupId::Boards), + ), + item( + "side.boards.delete", + "Delete board", + Side, + Board, + Some(ToolbarGroupId::Boards), + ), + item( + "side.boards.rename", + "Rename board", + Side, + Board, + Some(ToolbarGroupId::Boards), + ), + item( + "side.settings.context-aware-ui", + "Context UI", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.text-controls", + "Text controls", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.status-bar", + "Status bar", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.status-board-badge", + "Status board badge", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.status-page-badge", + "Status page badge", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.floating-badge-always", + "Floating board/page badge", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.preset-toasts", + "Preset toasts", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.presets", + "Presets toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.actions", + "Actions toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.zoom-actions", + "Zoom actions toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.advanced-actions", + "Advanced actions toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.boards", + "Boards toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.pages", + "Pages toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.step-controls", + "Step controls toggle", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.configurator", + "Open configurator", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.settings.config-file", + "Open config file", + Side, + Setting, + Some(ToolbarGroupId::Settings), + ), + item( + "side.session.open", + "Open session", + Side, + Session, + Some(ToolbarGroupId::Session), + ), + item( + "side.session.save-as", + "Save session as", + Side, + Session, + Some(ToolbarGroupId::Session), + ), + item( + "side.session.info", + "Session info", + Side, + Session, + Some(ToolbarGroupId::Session), + ), + item( + "side.session.clear", + "Clear session", + Side, + Session, + Some(ToolbarGroupId::Session), + ), + item( + "side.session.manager", + "Session manager", + Side, + Session, + Some(ToolbarGroupId::Session), + ), + item( + "side.tool-options.color", + "Color", + Side, + ToolOption, + Some(ToolbarGroupId::Colors), + ), + item( + "side.tool-options.thickness", + "Thickness", + Side, + ToolOption, + Some(ToolbarGroupId::Thickness), + ), + item( + "side.tool-options.marker-opacity", + "Marker opacity", + Side, + ToolOption, + Some(ToolbarGroupId::MarkerOpacity), + ), + item( + "side.tool-options.eraser-mode", + "Eraser mode", + Side, + ToolOption, + Some(ToolbarGroupId::EraserMode), + ), + item( + "side.tool-options.font-size", + "Font size", + Side, + ToolOption, + Some(ToolbarGroupId::TextSize), + ), + item( + "side.tool-options.font-family", + "Font family", + Side, + ToolOption, + Some(ToolbarGroupId::Font), + ), + item( + "side.tool-options.polygon-sides", + "Polygon sides", + Side, + ToolOption, + Some(ToolbarGroupId::PolygonSides), + ), + item( + "side.tool-options.arrow-labels", + "Arrow labels", + Side, + ToolOption, + Some(ToolbarGroupId::ArrowLabels), + ), + item( + "side.tool-options.step-marker-reset", + "Reset step marker", + Side, + ToolOption, + Some(ToolbarGroupId::StepMarkers), + ), +]; + +const fn item( + id: &'static str, + label: &'static str, + surface: ToolbarItemSurface, + category: ToolbarItemCategory, + group: Option, +) -> ToolbarItemDefinition { + ToolbarItemDefinition::new(id, label, surface, category, group) +} + +use ToolbarItemCategory::{ + Action, Board, Chrome, Group, Page, Session, Setting, Tool, ToolOption, Utility, +}; +use ToolbarItemSurface::{Side, Top}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn known_hidden_ids_resolve_and_unknown_ids_round_trip() { + let config = ToolbarItemsConfig { + hidden: vec![ + "side.actions.undo-all".to_string(), + "future.toolbar.item".to_string(), + ], + }; + + let resolved = config.resolved(); + + assert!(resolved.is_hidden("side.actions.undo-all".parse().expect("known id"))); + assert_eq!(resolved.unknown_hidden, vec!["future.toolbar.item"]); + } + + #[test] + fn default_hidden_items_hide_screenshot_tool() { + let resolved = ToolbarItemsConfig::default().resolved(); + + assert!(resolved.is_hidden("top.utility.screenshot".parse().expect("known id"))); + } + + #[test] + fn set_hidden_preserves_unknown_ids_while_mutating_known_ids() { + 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(), + ], + }; + + config.set_hidden("side.actions.undo-all".parse().expect("known id"), false); + config.set_hidden("top.tool.pen".parse().expect("known id"), true); + + assert_eq!( + config.hidden, + vec![ + "future.toolbar.item", + "side.pages.duplicate", + "top.tool.pen" + ] + ); + } + + #[test] + fn reset_known_hidden_restores_defaults_and_preserves_unknown_ids() { + let mut config = ToolbarItemsConfig { + hidden: vec![ + "future.toolbar.item".to_string(), + "side.actions.undo-all".to_string(), + ], + }; + + assert!(config.reset_known_hidden_to_defaults()); + assert_eq!( + config.hidden, + vec!["top.utility.screenshot", "future.toolbar.item"] + ); + assert!(!config.reset_known_hidden_to_defaults()); + } + + #[test] + fn toolbar_group_ids_include_step_markers_and_step_undo() { + assert_eq!( + "step-markers".parse::(), + Ok(ToolbarGroupId::StepMarkers) + ); + assert_eq!( + "step-undo".parse::(), + Ok(ToolbarGroupId::StepUndo) + ); + } + + #[test] + fn toolbar_item_definitions_are_unique_parseable_and_labeled() { + let mut seen = BTreeSet::new(); + + for definition in toolbar_item_definitions() { + assert!( + seen.insert(definition.id.as_str()), + "duplicate toolbar item id: {}", + definition.id + ); + assert_eq!( + definition.id.as_str().parse::(), + Ok(definition.id) + ); + assert!( + !definition.label.is_empty(), + "missing toolbar item label: {}", + definition.id + ); + } + } +} diff --git a/src/config/types/toolbar/mod.rs b/src/config/types/toolbar/mod.rs index f5c34e4e..f17b33ac 100644 --- a/src/config/types/toolbar/mod.rs +++ b/src/config/types/toolbar/mod.rs @@ -1,7 +1,12 @@ mod config; +mod items; mod mode; mod overrides; pub use config::ToolbarConfig; +pub use items::{ + ResolvedToolbarItems, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, + ToolbarItemId, ToolbarItemSurface, ToolbarItemsConfig, toolbar_item_definitions, +}; pub use mode::{ToolbarLayoutMode, ToolbarSectionDefaults}; pub use overrides::{ToolbarModeOverride, ToolbarModeOverrides}; diff --git a/src/config/validate/ui.rs b/src/config/validate/ui.rs index 1e5ebf36..62bef730 100644 --- a/src/config/validate/ui.rs +++ b/src/config/validate/ui.rs @@ -53,6 +53,13 @@ impl Config { self.ui.toolbar.scale = self.ui.toolbar.scale.clamp(0.5, 3.0); } + for unknown in self.ui.toolbar.items.resolved().unknown_hidden { + log::warn!( + "Unknown toolbar item id {:?} in ui.toolbar.items.hidden; preserving it for forward compatibility", + unknown + ); + } + for i in 0..4 { if !(0.0..=1.0).contains(&self.ui.click_highlight.fill_color[i]) { log::warn!( diff --git a/src/input/state/core/base/state/init.rs b/src/input/state/core/base/state/init.rs index 02466fca..d0303ee1 100644 --- a/src/input/state/core/base/state/init.rs +++ b/src/input/state/core/base/state/init.rs @@ -141,9 +141,13 @@ impl InputState { toolbar_scale: 1.0, toolbar_layout_mode: crate::config::ToolbarLayoutMode::Regular, toolbar_mode_overrides: crate::config::ToolbarModeOverrides::default(), + toolbar_items: crate::config::ToolbarItemsConfig::default(), + resolved_toolbar_items: crate::config::ToolbarItemsConfig::default().resolved(), toolbar_shapes_expanded: false, toolbar_drawer_open: false, toolbar_drawer_tab: ToolbarDrawerTab::View, + toolbar_customize_items_open: false, + toolbar_customize_items_group: None, screen_width: 0, screen_height: 0, show_active_output_badge: false, diff --git a/src/input/state/core/base/state/structs.rs b/src/input/state/core/base/state/structs.rs index ab320749..22e5eec1 100644 --- a/src/input/state/core/base/state/structs.rs +++ b/src/input/state/core/base/state/structs.rs @@ -20,7 +20,8 @@ use super::super::types::{ UiToastState, ZoomAction, }; use crate::config::{ - Action, BoardsConfig, KeyBinding, PresenterModeConfig, RadialMenuMouseBinding, ToolPresetConfig, + Action, BoardsConfig, KeyBinding, PresenterModeConfig, RadialMenuMouseBinding, + ResolvedToolbarItems, ToolPresetConfig, ToolbarItemsConfig, }; use crate::draw::frame::ShapeSnapshot; use crate::draw::{Color, DirtyTracker, EraserKind, FontDescriptor, Shape, ShapeId}; @@ -199,12 +200,20 @@ pub struct InputState { pub toolbar_layout_mode: crate::config::ToolbarLayoutMode, /// Optional per-mode overrides for toolbar sections pub toolbar_mode_overrides: crate::config::ToolbarModeOverrides, + /// Raw item-level toolbar visibility config, preserving unknown IDs. + pub toolbar_items: ToolbarItemsConfig, + /// Resolved known item-level toolbar visibility config. + pub resolved_toolbar_items: ResolvedToolbarItems, /// Whether the simple-mode shape picker is expanded pub toolbar_shapes_expanded: bool, /// Whether the toolbar drawer is open pub toolbar_drawer_open: bool, /// Active toolbar drawer tab pub toolbar_drawer_tab: ToolbarDrawerTab, + /// Whether the Settings drawer is showing the toolbar item customization sub-panel + pub toolbar_customize_items_open: bool, + /// Selected toolbar item customization group in the Settings drawer sub-panel + pub toolbar_customize_items_group: Option, /// Screen width in pixels (set by backend after configuration) pub screen_width: u32, /// Screen height in pixels (set by backend after configuration) diff --git a/src/input/state/core/base/types.rs b/src/input/state/core/base/types.rs index 6f715c4f..c578b912 100644 --- a/src/input/state/core/base/types.rs +++ b/src/input/state/core/base/types.rs @@ -187,13 +187,27 @@ pub enum OutputFocusAction { pub enum ToolbarDrawerTab { View, App, + Session, + Sections, + Customize, } impl ToolbarDrawerTab { + pub const ALL: [Self; 5] = [ + Self::View, + Self::App, + Self::Sections, + Self::Session, + Self::Customize, + ]; + pub fn label(self) -> &'static str { match self { Self::View => "Canvas", Self::App => "Settings", + Self::Session => "Session", + Self::Sections => "Sections", + Self::Customize => "Customize", } } } diff --git a/src/input/state/core/tool_controls/toolbar.rs b/src/input/state/core/tool_controls/toolbar.rs index 6dee96f0..aede9ba1 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; +use crate::config::{Action, ToolbarItemId}; impl InputState { /// Sets toolbar visibility flag (controls both top and side). Returns true if toggled. @@ -40,6 +40,7 @@ impl InputState { &mut self, layout_mode: crate::config::ToolbarLayoutMode, mode_overrides: crate::config::ToolbarModeOverrides, + items: crate::config::ToolbarItemsConfig, top_pinned: bool, side_pinned: bool, use_icons: bool, @@ -69,6 +70,8 @@ impl InputState { self.toolbar_scale = scale; self.toolbar_layout_mode = layout_mode; self.toolbar_mode_overrides = mode_overrides; + self.resolved_toolbar_items = items.resolved(); + self.toolbar_items = items; self.show_more_colors = show_more_colors; self.show_actions_section = show_actions_section; self.show_actions_advanced = show_actions_advanced; @@ -87,6 +90,27 @@ impl InputState { self.apply_toolbar_mode_overrides(layout_mode); } + pub fn set_toolbar_item_hidden(&mut self, id: ToolbarItemId, hidden: bool) -> bool { + if self.resolved_toolbar_items.is_hidden(id) == hidden { + return false; + } + + self.toolbar_items.set_hidden(id, hidden); + self.resolved_toolbar_items = self.toolbar_items.resolved(); + self.needs_redraw = true; + true + } + + pub fn reset_toolbar_item_hidden_overrides(&mut self) -> bool { + if !self.toolbar_items.reset_known_hidden_to_defaults() { + 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/input/state/tests/basics.rs b/src/input/state/tests/basics.rs index ed2e25c7..f09246f0 100644 --- a/src/input/state/tests/basics.rs +++ b/src/input/state/tests/basics.rs @@ -199,6 +199,7 @@ fn toolbar_toggle_handles_partial_visibility() { state.init_toolbar_from_config( crate::config::ToolbarLayoutMode::Regular, crate::config::ToolbarModeOverrides::default(), + crate::config::ToolbarItemsConfig::default(), true, // top_pinned false, // side_pinned true, // use_icons diff --git a/src/toolbar_icons/actions.rs b/src/toolbar_icons/actions.rs index 033b55ef..5ee70499 100644 --- a/src/toolbar_icons/actions.rs +++ b/src/toolbar_icons/actions.rs @@ -248,6 +248,62 @@ pub fn draw_icon_copy(ctx: &Context, x: f64, y: f64, size: f64) { let _ = ctx.stroke(); } +/// Draw a screenshot/camera icon. +pub fn draw_icon_screenshot(ctx: &Context, x: f64, y: f64, size: f64) { + let s = size; + let stroke = (s * 0.09).max(1.5); + ctx.set_line_width(stroke); + ctx.set_line_join(cairo::LineJoin::Round); + ctx.set_line_cap(cairo::LineCap::Round); + + ctx.rectangle(x + s * 0.18, y + s * 0.32, s * 0.64, s * 0.46); + let _ = ctx.stroke(); + + ctx.move_to(x + s * 0.34, y + s * 0.32); + ctx.line_to(x + s * 0.4, y + s * 0.22); + ctx.line_to(x + s * 0.6, y + s * 0.22); + ctx.line_to(x + s * 0.66, y + s * 0.32); + let _ = ctx.stroke(); + + ctx.arc(x + s * 0.5, y + s * 0.56, s * 0.13, 0.0, PI * 2.0); + let _ = ctx.stroke(); + ctx.arc(x + s * 0.72, y + s * 0.4, s * 0.035, 0.0, PI * 2.0); + let _ = ctx.fill(); +} + +/// Draw a visibility/eye icon. +pub fn draw_icon_visibility(ctx: &Context, x: f64, y: f64, size: f64) { + let s = size; + let stroke = (s * 0.09).max(1.5); + ctx.set_line_width(stroke); + ctx.set_line_cap(cairo::LineCap::Round); + ctx.set_line_join(cairo::LineJoin::Round); + + let cx = x + s * 0.5; + let cy = y + s * 0.5; + ctx.move_to(x + s * 0.14, cy); + ctx.curve_to( + x + s * 0.28, + y + s * 0.25, + x + s * 0.72, + y + s * 0.25, + x + s * 0.86, + cy, + ); + ctx.curve_to( + x + s * 0.72, + y + s * 0.75, + x + s * 0.28, + y + s * 0.75, + x + s * 0.14, + cy, + ); + let _ = ctx.stroke(); + + ctx.arc(cx, cy, s * 0.14, 0.0, PI * 2.0); + let _ = ctx.stroke(); +} + /// Draw a left chevron/arrow icon for navigation. pub fn draw_icon_chevron_left(ctx: &Context, x: f64, y: f64, size: f64) { let s = size; diff --git a/src/ui/toolbar/apply/actions.rs b/src/ui/toolbar/apply/actions.rs index 75c8ca19..dab48e28 100644 --- a/src/ui/toolbar/apply/actions.rs +++ b/src/ui/toolbar/apply/actions.rs @@ -1,3 +1,4 @@ +use crate::config::Action; use crate::input::{InputState, ZoomAction}; impl InputState { @@ -46,6 +47,11 @@ impl InputState { true } + pub(super) fn apply_toolbar_capture_screenshot(&mut self) -> bool { + self.handle_action(Action::CaptureSelection); + true + } + pub(super) fn apply_toolbar_toggle_freeze(&mut self) -> bool { self.request_frozen_toggle(); self.needs_redraw = true; diff --git a/src/ui/toolbar/apply/layout.rs b/src/ui/toolbar/apply/layout.rs index 2c5f0306..1eb1f3c8 100644 --- a/src/ui/toolbar/apply/layout.rs +++ b/src/ui/toolbar/apply/layout.rs @@ -1,6 +1,6 @@ -use crate::config::ToolbarLayoutMode; +use crate::config::{ToolbarItemId, ToolbarLayoutMode}; use crate::input::{InputState, ToolbarDrawerTab}; -use crate::ui::toolbar::ToolbarSideSection; +use crate::ui::toolbar::{ToolbarItemCustomizeGroup, ToolbarSideSection}; impl InputState { pub(super) fn apply_toolbar_toggle_custom_section(&mut self, enable: bool) -> bool { @@ -247,6 +247,10 @@ impl InputState { pub(super) fn apply_toolbar_toggle_drawer(&mut self, open: bool) -> bool { if self.toolbar_drawer_open != open { self.toolbar_drawer_open = open; + if !open { + self.toolbar_customize_items_open = false; + self.toolbar_customize_items_group = None; + } self.needs_redraw = true; true } else { @@ -260,6 +264,28 @@ impl InputState { self.toolbar_drawer_tab = tab; changed = true; } + match tab { + ToolbarDrawerTab::Customize => { + if !self.toolbar_customize_items_open + || self.toolbar_customize_items_group.is_some() + { + self.toolbar_customize_items_open = true; + self.toolbar_customize_items_group = None; + changed = true; + } + } + ToolbarDrawerTab::View + | ToolbarDrawerTab::App + | ToolbarDrawerTab::Session + | ToolbarDrawerTab::Sections => { + if self.toolbar_customize_items_open || self.toolbar_customize_items_group.is_some() + { + self.toolbar_customize_items_open = false; + self.toolbar_customize_items_group = None; + changed = true; + } + } + } if !self.toolbar_drawer_open { self.toolbar_drawer_open = true; changed = true; @@ -299,6 +325,49 @@ impl InputState { } } + pub(super) fn apply_toolbar_set_item_hidden( + &mut self, + id: ToolbarItemId, + hidden: bool, + ) -> bool { + self.set_toolbar_item_hidden(id, hidden) + } + + pub(super) fn apply_toolbar_reset_item_hidden_overrides(&mut self) -> bool { + self.reset_toolbar_item_hidden_overrides() + } + + pub(super) fn apply_toolbar_set_item_customization_open(&mut self, open: bool) -> bool { + if self.toolbar_customize_items_open == open { + return false; + } + self.toolbar_customize_items_open = open; + if !open { + self.toolbar_customize_items_group = None; + } + self.toolbar_drawer_open = true; + self.toolbar_drawer_tab = ToolbarDrawerTab::App; + self.needs_redraw = true; + true + } + + pub(super) fn apply_toolbar_set_item_customization_group( + &mut self, + group: Option, + ) -> bool { + if self.toolbar_customize_items_group == group && self.toolbar_customize_items_open { + return false; + } + self.toolbar_customize_items_open = true; + self.toolbar_customize_items_group = group; + self.toolbar_drawer_open = true; + if self.toolbar_drawer_tab != ToolbarDrawerTab::Customize { + self.toolbar_drawer_tab = ToolbarDrawerTab::App; + } + self.needs_redraw = true; + true + } + pub(super) fn apply_toolbar_toggle_shape_picker(&mut self, open: bool) -> bool { if self.toolbar_shapes_expanded != open { self.toolbar_shapes_expanded = open; diff --git a/src/ui/toolbar/apply/mod.rs b/src/ui/toolbar/apply/mod.rs index b7378318..8358a590 100644 --- a/src/ui/toolbar/apply/mod.rs +++ b/src/ui/toolbar/apply/mod.rs @@ -57,6 +57,7 @@ impl InputState { ToolbarEvent::CustomUndo => self.apply_toolbar_custom_undo(), ToolbarEvent::CustomRedo => self.apply_toolbar_custom_redo(), ToolbarEvent::ClearCanvas => self.apply_toolbar_clear_canvas(), + ToolbarEvent::CaptureScreenshot => self.apply_toolbar_capture_screenshot(), ToolbarEvent::PagePrev => self.apply_toolbar_page_prev(), ToolbarEvent::PageNext => self.apply_toolbar_page_next(), ToolbarEvent::PageNew => self.apply_toolbar_page_new(), @@ -135,6 +136,18 @@ impl InputState { self.apply_toolbar_toggle_side_section_collapsed(section, collapsed) } ToolbarEvent::SetToolbarLayoutMode(mode) => self.apply_toolbar_set_layout_mode(mode), + ToolbarEvent::SetToolbarItemHidden(id, hidden) => { + self.apply_toolbar_set_item_hidden(id, hidden) + } + ToolbarEvent::ResetToolbarItemHiddenOverrides => { + self.apply_toolbar_reset_item_hidden_overrides() + } + ToolbarEvent::SetToolbarItemCustomizationOpen(open) => { + self.apply_toolbar_set_item_customization_open(open) + } + ToolbarEvent::SetToolbarItemCustomizationGroup(group) => { + self.apply_toolbar_set_item_customization_group(group) + } ToolbarEvent::ToggleShapePicker(open) => self.apply_toolbar_toggle_shape_picker(open), ToolbarEvent::ApplyPreset(slot) => self.apply_toolbar_apply_preset(slot), ToolbarEvent::SavePreset(slot) => self.apply_toolbar_save_preset(slot), diff --git a/src/ui/toolbar/events.rs b/src/ui/toolbar/events.rs index c634dd25..61a30104 100644 --- a/src/ui/toolbar/events.rs +++ b/src/ui/toolbar/events.rs @@ -1,11 +1,40 @@ use std::path::PathBuf; -use crate::config::{Action, ToolbarLayoutMode}; +use crate::config::{Action, ToolbarItemId, ToolbarLayoutMode}; use crate::draw::{Color, FontDescriptor}; use crate::input::{EraserMode, Tool, ToolbarDrawerTab}; use super::ToolbarSnapshot; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ToolbarItemCustomizeGroup { + TopTools, + TopControls, + SideSections, + Actions, + Pages, + Boards, + Presets, + ToolOptions, + Sessions, +} + +impl ToolbarItemCustomizeGroup { + pub const fn label(self) -> &'static str { + match self { + Self::TopTools => "Top tools", + Self::TopControls => "Top controls", + Self::SideSections => "Side sections", + Self::Actions => "Actions", + Self::Pages => "Pages", + Self::Boards => "Boards", + Self::Presets => "Presets", + Self::ToolOptions => "Tool options", + Self::Sessions => "Sessions", + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ToolbarSideSection { Colors, @@ -76,6 +105,7 @@ pub enum ToolbarEvent { Undo, Redo, ClearCanvas, + CaptureScreenshot, PagePrev, PageNext, PageNew, @@ -180,6 +210,14 @@ pub enum ToolbarEvent { ToggleSideSectionCollapsed(ToolbarSideSection, bool), /// Set toolbar layout mode SetToolbarLayoutMode(ToolbarLayoutMode), + /// Hide or show a known toolbar item override. + SetToolbarItemHidden(ToolbarItemId, bool), + /// Clear known hidden toolbar item overrides, preserving unknown/future IDs. + ResetToolbarItemHiddenOverrides, + /// Show or hide the Settings drawer toolbar-item customization sub-panel. + SetToolbarItemCustomizationOpen(bool), + /// Select the Settings drawer toolbar-item customization group. + SetToolbarItemCustomizationGroup(Option), /// Toggle the simple-mode shape picker ToggleShapePicker(bool), /// Drag handle for top toolbar (toolbar coords; screen coords when inline toolbars are active) diff --git a/src/ui/toolbar/mod.rs b/src/ui/toolbar/mod.rs index 2f1a8715..ac3355f6 100644 --- a/src/ui/toolbar/mod.rs +++ b/src/ui/toolbar/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod model; pub mod snapshot; pub use bindings::ToolbarBindingHints; -pub use events::{ToolbarEvent, ToolbarSideSection}; +pub use events::{ToolbarEvent, ToolbarItemCustomizeGroup, ToolbarSideSection}; #[allow(unused_imports)] pub use snapshot::{ PresetFeedbackSnapshot, PresetSlotSnapshot, SessionRecentSnapshot, ToolContext, diff --git a/src/ui/toolbar/model/actions.rs b/src/ui/toolbar/model/actions.rs index 157862b7..33eefc1d 100644 --- a/src/ui/toolbar/model/actions.rs +++ b/src/ui/toolbar/model/actions.rs @@ -1,6 +1,7 @@ +use crate::config::ToolbarItemId; use crate::input::ToolbarDrawerTab; -use super::super::{ToolbarEvent, ToolbarSnapshot}; +use super::super::{ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ToolbarCommandGroupKind { @@ -62,6 +63,10 @@ pub(crate) struct ToolbarActionsModel { impl ToolbarActionsModel { pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Option { + if snapshot.side_section_hidden(ToolbarSideSection::Actions) { + return None; + } + let show_drawer_view = drawer_view_visible(snapshot); let show_advanced = snapshot.show_actions_advanced && show_drawer_view; let show_view_actions = show_drawer_view @@ -75,18 +80,22 @@ impl ToolbarActionsModel { let mut groups = Vec::with_capacity(3); if snapshot.show_actions_section { - groups.push(ToolbarCommandGroup::new( + push_visible_group( + snapshot, + &mut groups, ToolbarCommandGroupKind::BasicActions, vec![ ToolbarButtonModel::new(ToolbarEvent::Undo, snapshot.undo_available), ToolbarButtonModel::new(ToolbarEvent::Redo, snapshot.redo_available), ToolbarButtonModel::new(ToolbarEvent::ClearCanvas, true), ], - )); + ); } if show_view_actions { - groups.push(ToolbarCommandGroup::new( + push_visible_group( + snapshot, + &mut groups, ToolbarCommandGroupKind::ViewActions, vec![ ToolbarButtonModel::new(ToolbarEvent::ZoomIn, true), @@ -94,7 +103,7 @@ impl ToolbarActionsModel { ToolbarButtonModel::new(ToolbarEvent::ResetZoom, snapshot.zoom_active), ToolbarButtonModel::new(ToolbarEvent::ToggleZoomLock, snapshot.zoom_active), ], - )); + ); } if show_advanced { @@ -118,13 +127,15 @@ impl ToolbarActionsModel { )); } buttons.push(ToolbarButtonModel::new(ToolbarEvent::ToggleFreeze, true)); - groups.push(ToolbarCommandGroup::new( + push_visible_group( + snapshot, + &mut groups, ToolbarCommandGroupKind::AdvancedActions, buttons, - )); + ); } - Some(Self { groups }) + (!groups.is_empty()).then_some(Self { groups }) } pub(crate) fn groups(&self) -> &[ToolbarCommandGroup] { @@ -133,11 +144,15 @@ impl ToolbarActionsModel { } pub(crate) fn toolbar_pages_model(snapshot: &ToolbarSnapshot) -> Option { - if !snapshot.show_pages_section || !drawer_view_visible(snapshot) { + if snapshot.side_section_hidden(ToolbarSideSection::Pages) + || !snapshot.show_pages_section + || !drawer_view_visible(snapshot) + { return None; } - Some(ToolbarCommandGroup::new( + visible_group( + snapshot, ToolbarCommandGroupKind::Pages, vec![ ToolbarButtonModel::new(ToolbarEvent::PagePrev, snapshot.page_index > 0), @@ -149,16 +164,20 @@ pub(crate) fn toolbar_pages_model(snapshot: &ToolbarSnapshot) -> Option Option { - if !snapshot.show_boards_section || !drawer_view_visible(snapshot) { + if snapshot.side_section_hidden(ToolbarSideSection::Boards) + || !snapshot.show_boards_section + || !drawer_view_visible(snapshot) + { return None; } let can_cycle = snapshot.board_count > 1; - Some(ToolbarCommandGroup::new( + visible_group( + snapshot, ToolbarCommandGroupKind::Boards, vec![ ToolbarButtonModel::new(ToolbarEvent::BoardPrev, can_cycle), @@ -167,9 +186,65 @@ pub(crate) fn toolbar_boards_model(snapshot: &ToolbarSnapshot) -> Option bool { snapshot.drawer_open && snapshot.drawer_tab == ToolbarDrawerTab::View } + +fn push_visible_group( + snapshot: &ToolbarSnapshot, + groups: &mut Vec, + kind: ToolbarCommandGroupKind, + buttons: Vec, +) { + if let Some(group) = visible_group(snapshot, kind, buttons) { + groups.push(group); + } +} + +fn visible_group( + snapshot: &ToolbarSnapshot, + kind: ToolbarCommandGroupKind, + buttons: Vec, +) -> Option { + let buttons: Vec<_> = buttons + .into_iter() + .filter(|button| toolbar_button_visible(snapshot, &button.event)) + .collect(); + (!buttons.is_empty()).then(|| ToolbarCommandGroup::new(kind, buttons)) +} + +fn toolbar_button_visible(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> bool { + toolbar_button_item_id(event).is_none_or(|id| !snapshot.toolbar_item_hidden(id)) +} + +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", + _ => return None, + })) +} diff --git a/src/ui/toolbar/model/activation.rs b/src/ui/toolbar/model/activation.rs index 51dbc891..5b366b2d 100644 --- a/src/ui/toolbar/model/activation.rs +++ b/src/ui/toolbar/model/activation.rs @@ -31,6 +31,9 @@ pub(crate) enum ToolbarControlId { SettingsBoards, SettingsPages, SettingsStepControls, + CustomizeToolbarItems, + BackToolbarSettings, + ResetToolbarHiddenItems, OpenConfigurator, OpenConfigFile, } diff --git a/src/ui/toolbar/model/control.rs b/src/ui/toolbar/model/control.rs index b9c4063c..209682ad 100644 --- a/src/ui/toolbar/model/control.rs +++ b/src/ui/toolbar/model/control.rs @@ -151,7 +151,9 @@ pub(crate) enum ToolbarControlRole { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ToolbarIcon { + Back, Settings, + Visibility, File, More, Board, diff --git a/src/ui/toolbar/model/event_policy.rs b/src/ui/toolbar/model/event_policy.rs index 08c0a2b1..6a88a457 100644 --- a/src/ui/toolbar/model/event_policy.rs +++ b/src/ui/toolbar/model/event_policy.rs @@ -72,6 +72,7 @@ pub(crate) fn action_for_event(event: &ToolbarEvent) -> Option { ToolbarEvent::UndoAllDelayed => Some(Action::UndoAllDelayed), ToolbarEvent::RedoAllDelayed => Some(Action::RedoAllDelayed), ToolbarEvent::ClearCanvas => Some(Action::ClearCanvas), + ToolbarEvent::CaptureScreenshot => Some(Action::CaptureSelection), ToolbarEvent::PagePrev => Some(Action::PagePrev), ToolbarEvent::PageNext => Some(Action::PageNext), ToolbarEvent::PageNew => Some(Action::PageNew), @@ -185,7 +186,9 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence { | ToolbarEvent::TogglePresetToasts(_) | ToolbarEvent::ToggleToolPreview(_) | ToolbarEvent::ToggleDelaySliders(_) - | ToolbarEvent::SetToolbarLayoutMode(_) => ToolbarPersistence::Persist(Toolbar), + | ToolbarEvent::SetToolbarLayoutMode(_) + | ToolbarEvent::SetToolbarItemHidden(_, _) + | ToolbarEvent::ResetToolbarItemHiddenOverrides => ToolbarPersistence::Persist(Toolbar), ToolbarEvent::ToggleCustomSection(_) => ToolbarPersistence::Persist(History), ToolbarEvent::ToggleStatusBar(_) => ToolbarPersistence::Persist(Ui(StatusBar)), ToolbarEvent::ToggleStatusBoardBadge(_) => { diff --git a/src/ui/toolbar/model/mod.rs b/src/ui/toolbar/model/mod.rs index 4431b189..da128e58 100644 --- a/src/ui/toolbar/model/mod.rs +++ b/src/ui/toolbar/model/mod.rs @@ -40,7 +40,11 @@ pub(crate) use settings::{ToolbarSettingsButton, ToolbarSettingsModel, ToolbarSe pub(crate) use tools::{ SemanticToolIcon, common_shape_tools, 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, top_tool_buttons, + 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, }; #[cfg(test)] @@ -189,6 +193,15 @@ mod tests { snapshot.layout_mode = ToolbarLayoutMode::Regular; let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + assert!( + !model + .toggles() + .iter() + .any(|toggle| toggle.id == ToolbarControlId::SettingsAdvancedActions) + ); + + snapshot.drawer_tab = ToolbarDrawerTab::Sections; + let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); assert!( model .toggles() @@ -197,6 +210,61 @@ mod tests { ); } + #[test] + fn settings_model_moves_hidden_item_overrides_into_customization_panel() { + let mut snapshot = snapshot(); + snapshot.drawer_tab = ToolbarDrawerTab::App; + snapshot.show_settings_section = true; + snapshot.resolved_toolbar_items = crate::config::ToolbarItemsConfig { + hidden: vec!["top.tool.pen".to_string()], + } + .resolved(); + + let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + assert!(model.item_overrides().is_empty()); + assert!(model.buttons().iter().any(|button| { + matches!( + &button.event, + ToolbarEvent::SetToolbarItemCustomizationOpen(true) + ) + })); + assert!( + model.buttons().iter().any(|button| matches!( + &button.event, + ToolbarEvent::ResetToolbarItemHiddenOverrides + )) + ); + + snapshot.customize_items_open = true; + let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + assert!(model.item_overrides().is_empty()); + assert!( + model + .groups() + .iter() + .any(|group| group.label.as_ref() == "Top tools") + ); + + snapshot.customize_items_group = + Some(crate::ui::toolbar::ToolbarItemCustomizeGroup::TopTools); + let model = ToolbarSettingsModel::from_snapshot(&snapshot).expect("settings"); + assert!(model.groups().is_empty()); + assert!( + model + .item_overrides() + .iter() + .any(|item| item.id.as_str() == "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.") + })); + assert!(model.buttons().iter().any(|button| matches!( + &button.event, + ToolbarEvent::SetToolbarItemCustomizationGroup(None) + ))); + } + #[test] fn event_policy_classifies_persistence_and_pre_apply_effects() { assert_eq!( diff --git a/src/ui/toolbar/model/session.rs b/src/ui/toolbar/model/session.rs index 4914759f..5347e826 100644 --- a/src/ui/toolbar/model/session.rs +++ b/src/ui/toolbar/model/session.rs @@ -1,8 +1,9 @@ use std::path::PathBuf; +use crate::config::ToolbarItemId; use crate::input::ToolbarDrawerTab; -use super::super::{SessionRecentSnapshot, ToolbarEvent, ToolbarSnapshot}; +use super::super::{SessionRecentSnapshot, ToolbarEvent, ToolbarSideSection, ToolbarSnapshot}; const MAX_RECENT_SESSIONS: usize = 3; pub(crate) const SESSION_BUTTON_COLUMNS: usize = 3; @@ -18,7 +19,10 @@ pub(crate) struct ToolbarSessionModel { impl ToolbarSessionModel { pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Option { - if !snapshot.drawer_open || snapshot.drawer_tab != ToolbarDrawerTab::App { + if snapshot.side_section_hidden(ToolbarSideSection::Session) + || !snapshot.drawer_open + || snapshot.drawer_tab != ToolbarDrawerTab::Session + { return None; } @@ -32,13 +36,16 @@ impl ToolbarSessionModel { .map(|path| path.display().to_string()) .unwrap_or_else(|| "No persisted session target".to_string()); let target_active = snapshot.active_session_path.is_some(); - let buttons = vec![ + let buttons: Vec<_> = vec![ ToolbarSessionButton::new(ToolbarEvent::OpenSession, "Open", target_active), ToolbarSessionButton::new(ToolbarEvent::SaveSessionAs, "Save As", target_active), ToolbarSessionButton::new(ToolbarEvent::SessionInfo, "Info", target_active), ToolbarSessionButton::new(ToolbarEvent::ClearSession, "Clear", target_active), ToolbarSessionButton::new(ToolbarEvent::OpenConfigurator, "Manager", true), - ]; + ] + .into_iter() + .filter(|button| session_button_visible(snapshot, &button.event)) + .collect(); let recents = if target_active { snapshot .recent_sessions @@ -57,7 +64,7 @@ impl ToolbarSessionModel { path: path.clone(), }); - Some(Self { + (!buttons.is_empty() || !recents.is_empty()).then_some(Self { active_name, active_path_label, buttons, @@ -138,6 +145,21 @@ fn session_path_label(path: &std::path::Path) -> String { .unwrap_or_else(|| path.display().to_string()) } +fn session_button_visible(snapshot: &ToolbarSnapshot, event: &ToolbarEvent) -> bool { + session_button_item_id(event).is_none_or(|id| !snapshot.toolbar_item_hidden(id)) +} + +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", + _ => return None, + })) +} + #[cfg(test)] mod tests { use super::*; @@ -147,7 +169,7 @@ mod tests { fn app_snapshot() -> ToolbarSnapshot { let mut state = make_test_input_state(); state.toolbar_drawer_open = true; - state.toolbar_drawer_tab = ToolbarDrawerTab::App; + state.toolbar_drawer_tab = ToolbarDrawerTab::Session; ToolbarSnapshot::from_input_with_bindings(&state, ToolbarBindingHints::default()) } diff --git a/src/ui/toolbar/model/settings.rs b/src/ui/toolbar/model/settings.rs index 6cb01608..b64267c8 100644 --- a/src/ui/toolbar/model/settings.rs +++ b/src/ui/toolbar/model/settings.rs @@ -1,8 +1,12 @@ use std::borrow::Cow; -use crate::config::{Action, ToolbarLayoutMode, action_label, action_short_label}; +use crate::config::{ + Action, ToolbarGroupId, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemId, + ToolbarItemSurface, ToolbarLayoutMode, action_label, action_short_label, + toolbar_item_definitions, +}; -use super::super::{ToolbarEvent, ToolbarSnapshot}; +use super::super::{ToolbarEvent, ToolbarItemCustomizeGroup, ToolbarSideSection, ToolbarSnapshot}; use super::activation::{ToolbarActivation, ToolbarControlId}; use super::control::{ToolbarIcon, ToolbarTooltip}; @@ -10,13 +14,21 @@ use super::control::{ToolbarIcon, ToolbarTooltip}; pub(crate) struct ToolbarSettingsModel { toggles: Vec, buttons: Vec, + groups: Vec, + item_overrides: Vec, } impl ToolbarSettingsModel { pub(crate) fn from_snapshot(snapshot: &ToolbarSnapshot) -> Option { - if !snapshot.show_settings_section - || !snapshot.drawer_open - || snapshot.drawer_tab != crate::input::ToolbarDrawerTab::App + let customize_shortcut = snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Customize; + let sections_tab = snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Sections; + let customizing = snapshot.customize_items_open || customize_shortcut; + if !snapshot.drawer_open + || (!customize_shortcut + && !sections_tab + && (snapshot.side_section_hidden(ToolbarSideSection::Settings) + || !snapshot.show_settings_section + || snapshot.drawer_tab != crate::input::ToolbarDrawerTab::App)) { return None; } @@ -127,30 +139,45 @@ impl ToolbarSettingsModel { ]); } - Some(Self { + if sections_tab { + toggles.retain(|toggle| is_section_toggle_id(toggle.id)); + } else { + toggles.retain(|toggle| !is_section_toggle_id(toggle.id)); + } + toggles.retain(|toggle| control_visible(snapshot, toggle.id)); + if customizing { + toggles.clear(); + } + + let buttons = if customizing { + customize_buttons(snapshot) + } else if sections_tab { + section_buttons(snapshot) + } else { + settings_buttons(snapshot) + }; + + let groups = if customizing && snapshot.customize_items_group.is_none() { + customize_groups() + } else { + Vec::new() + }; + + let item_overrides: Vec<_> = if let Some(group) = snapshot.customize_items_group { + toolbar_item_definitions() + .iter() + .filter(|definition| customize_group_contains(group, definition)) + .map(|definition| ToolbarSettingsItemOverride::new(snapshot, definition)) + .collect() + } else { + Vec::new() + }; + + (!toggles.is_empty() || !buttons.is_empty() || !item_overrides.is_empty()).then_some(Self { toggles, - buttons: vec![ - ToolbarSettingsButton { - id: ToolbarControlId::OpenConfigurator, - label: Cow::Borrowed(action_short_label(Action::OpenConfigurator)), - event: ToolbarEvent::OpenConfigurator, - icon: ToolbarIcon::Settings, - tooltip: ToolbarTooltip::Binding { - label: Cow::Borrowed(action_label(Action::OpenConfigurator)), - binding: snapshot - .binding_hints - .binding_for_action(Action::OpenConfigurator) - .map(str::to_string), - }, - }, - ToolbarSettingsButton { - id: ToolbarControlId::OpenConfigFile, - label: Cow::Borrowed("Config file"), - event: ToolbarEvent::OpenConfigFile, - icon: ToolbarIcon::File, - tooltip: ToolbarTooltip::text("Config file"), - }, - ], + buttons, + groups, + item_overrides, }) } @@ -161,6 +188,235 @@ impl ToolbarSettingsModel { pub(crate) fn buttons(&self) -> &[ToolbarSettingsButton] { &self.buttons } + + pub(crate) fn groups(&self) -> &[ToolbarSettingsCustomizeGroup] { + &self.groups + } + + pub(crate) fn item_overrides(&self) -> &[ToolbarSettingsItemOverride] { + &self.item_overrides + } +} + +fn settings_buttons(snapshot: &ToolbarSnapshot) -> Vec { + vec![ + ToolbarSettingsButton { + id: ToolbarControlId::CustomizeToolbarItems, + label: Cow::Borrowed("Customize toolbar"), + event: ToolbarEvent::SetToolbarItemCustomizationOpen(true), + icon: ToolbarIcon::Visibility, + tooltip: ToolbarTooltip::text("Customize toolbar item visibility"), + }, + ToolbarSettingsButton { + id: ToolbarControlId::ResetToolbarHiddenItems, + label: Cow::Borrowed("Reset hidden"), + event: ToolbarEvent::ResetToolbarItemHiddenOverrides, + icon: ToolbarIcon::Visibility, + tooltip: ToolbarTooltip::text("Restore default hidden items"), + }, + ToolbarSettingsButton { + id: ToolbarControlId::OpenConfigurator, + label: Cow::Borrowed(action_short_label(Action::OpenConfigurator)), + event: ToolbarEvent::OpenConfigurator, + icon: ToolbarIcon::Settings, + tooltip: ToolbarTooltip::Binding { + label: Cow::Borrowed(action_label(Action::OpenConfigurator)), + binding: snapshot + .binding_hints + .binding_for_action(Action::OpenConfigurator) + .map(str::to_string), + }, + }, + ToolbarSettingsButton { + id: ToolbarControlId::OpenConfigFile, + label: Cow::Borrowed("Config file"), + event: ToolbarEvent::OpenConfigFile, + icon: ToolbarIcon::File, + tooltip: ToolbarTooltip::text("Config file"), + }, + ] + .into_iter() + .filter(|button| reset_button_visible(snapshot, button.id)) + .filter(|button| control_visible(snapshot, button.id)) + .collect() +} + +fn section_buttons(snapshot: &ToolbarSnapshot) -> Vec { + vec![ToolbarSettingsButton { + id: ToolbarControlId::ResetToolbarHiddenItems, + label: Cow::Borrowed("Reset hidden"), + event: ToolbarEvent::ResetToolbarItemHiddenOverrides, + icon: ToolbarIcon::Visibility, + tooltip: ToolbarTooltip::text("Restore default hidden items"), + }] + .into_iter() + .filter(|button| reset_button_visible(snapshot, button.id)) + .collect() +} + +fn customize_buttons(snapshot: &ToolbarSnapshot) -> Vec { + let back_event = if snapshot.customize_items_group.is_some() { + ToolbarEvent::SetToolbarItemCustomizationGroup(None) + } else if snapshot.drawer_tab == crate::input::ToolbarDrawerTab::Customize { + ToolbarEvent::SetDrawerTab(crate::input::ToolbarDrawerTab::App) + } else { + ToolbarEvent::SetToolbarItemCustomizationOpen(false) + }; + vec![ + ToolbarSettingsButton { + id: ToolbarControlId::BackToolbarSettings, + label: Cow::Borrowed("Back"), + event: back_event, + icon: ToolbarIcon::Back, + tooltip: ToolbarTooltip::text("Back to settings"), + }, + ToolbarSettingsButton { + id: ToolbarControlId::ResetToolbarHiddenItems, + label: Cow::Borrowed("Reset hidden"), + event: ToolbarEvent::ResetToolbarItemHiddenOverrides, + icon: ToolbarIcon::Visibility, + tooltip: ToolbarTooltip::text("Restore default hidden items"), + }, + ] + .into_iter() + .filter(|button| reset_button_visible(snapshot, button.id)) + .collect() +} + +fn reset_button_visible(snapshot: &ToolbarSnapshot, id: ToolbarControlId) -> bool { + id != ToolbarControlId::ResetToolbarHiddenItems + || !snapshot.resolved_toolbar_items.hidden.is_empty() +} + +fn is_section_toggle_id(id: ToolbarControlId) -> bool { + matches!( + id, + ToolbarControlId::SettingsPresets + | ToolbarControlId::SettingsActions + | ToolbarControlId::SettingsZoomActions + | ToolbarControlId::SettingsAdvancedActions + | ToolbarControlId::SettingsBoards + | ToolbarControlId::SettingsPages + | ToolbarControlId::SettingsStepControls + ) +} + +fn overlay_item_override_allowed(definition: &ToolbarItemDefinition) -> bool { + definition.group != Some(ToolbarGroupId::Settings) +} + +fn customize_groups() -> Vec { + [ + ToolbarItemCustomizeGroup::TopTools, + ToolbarItemCustomizeGroup::TopControls, + ToolbarItemCustomizeGroup::SideSections, + ToolbarItemCustomizeGroup::Actions, + ToolbarItemCustomizeGroup::Pages, + ToolbarItemCustomizeGroup::Boards, + ToolbarItemCustomizeGroup::Presets, + ToolbarItemCustomizeGroup::ToolOptions, + ToolbarItemCustomizeGroup::Sessions, + ] + .into_iter() + .map(ToolbarSettingsCustomizeGroup::new) + .collect() +} + +fn customize_group_contains( + group: ToolbarItemCustomizeGroup, + definition: &ToolbarItemDefinition, +) -> bool { + if !overlay_item_override_allowed(definition) { + return false; + } + + match group { + ToolbarItemCustomizeGroup::TopTools => { + definition.surface == ToolbarItemSurface::Top + && definition.category == ToolbarItemCategory::Tool + } + ToolbarItemCustomizeGroup::TopControls => { + definition.surface == ToolbarItemSurface::Top + && definition.category != ToolbarItemCategory::Tool + } + ToolbarItemCustomizeGroup::SideSections => { + definition.category == ToolbarItemCategory::Group + } + ToolbarItemCustomizeGroup::Actions => definition.category == ToolbarItemCategory::Action, + ToolbarItemCustomizeGroup::Pages => definition.category == ToolbarItemCategory::Page, + ToolbarItemCustomizeGroup::Boards => definition.category == ToolbarItemCategory::Board, + ToolbarItemCustomizeGroup::Presets => definition.group == Some(ToolbarGroupId::Presets), + ToolbarItemCustomizeGroup::ToolOptions => { + definition.category == ToolbarItemCategory::ToolOption + } + ToolbarItemCustomizeGroup::Sessions => definition.category == ToolbarItemCategory::Session, + } +} + +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", + _ => return None, + })) +} + +#[derive(Debug, Clone)] +pub(crate) struct ToolbarSettingsCustomizeGroup { + pub(crate) label: Cow<'static, str>, + pub(crate) event: ToolbarEvent, + pub(crate) tooltip: ToolbarTooltip, +} + +impl ToolbarSettingsCustomizeGroup { + fn new(group: ToolbarItemCustomizeGroup) -> Self { + Self { + label: Cow::Borrowed(group.label()), + event: ToolbarEvent::SetToolbarItemCustomizationGroup(Some(group)), + tooltip: ToolbarTooltip::text(format!("Customize {}", group.label())), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct ToolbarSettingsItemOverride { + pub(crate) id: ToolbarItemId, + pub(crate) label: Cow<'static, str>, + pub(crate) shown: bool, + pub(crate) activation: ToolbarActivation, + pub(crate) tooltip: ToolbarTooltip, +} + +impl ToolbarSettingsItemOverride { + fn new(snapshot: &ToolbarSnapshot, definition: &ToolbarItemDefinition) -> Self { + let id = definition.id; + let hidden = snapshot.toolbar_item_hidden(id); + 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)), + } + } } #[derive(Debug, Clone)] diff --git a/src/ui/toolbar/model/tools.rs b/src/ui/toolbar/model/tools.rs index d3c9a15a..167ad021 100644 --- a/src/ui/toolbar/model/tools.rs +++ b/src/ui/toolbar/model/tools.rs @@ -1,4 +1,6 @@ +use crate::config::ToolbarItemId; use crate::input::Tool; +use crate::ui::toolbar::ToolbarSnapshot; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SemanticToolIcon { @@ -78,6 +80,96 @@ pub(crate) fn top_tool_buttons(simple: bool) -> &'static [Tool] { } } +pub(crate) fn visible_top_tool_buttons( + simple: bool, + snapshot: &ToolbarSnapshot, +) -> impl Iterator + '_ { + visible_tools(top_tool_buttons(simple), snapshot) +} + +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)) +} + +pub(crate) fn visible_tool_count(tools: &'static [Tool], snapshot: &ToolbarSnapshot) -> usize { + visible_tools(tools, snapshot).count() +} + +pub(crate) fn tool_visible(snapshot: &ToolbarSnapshot, tool: Tool) -> bool { + toolbar_item_visible(snapshot, toolbar_item_id_for_tool(tool).as_str()) +} + +pub(crate) fn toolbar_item_visible(snapshot: &ToolbarSnapshot, id: &'static str) -> bool { + !snapshot.toolbar_item_hidden(ToolbarItemId::from_known(id)) +} + +pub(crate) fn top_shape_picker_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.shape-picker") +} + +pub(crate) fn top_fill_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.fill") +} + +pub(crate) fn top_text_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.text") +} + +pub(crate) fn top_sticky_note_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.sticky-note") +} + +pub(crate) fn top_clear_canvas_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.clear-canvas") +} + +pub(crate) fn top_screenshot_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.screenshot") +} + +pub(crate) fn top_highlight_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "top.utility.highlight") +} + +pub(crate) fn top_highlight_ring_visible(snapshot: &ToolbarSnapshot) -> bool { + toolbar_item_visible(snapshot, "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") + } else { + toolbar_item_visible(snapshot, "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 shape_tools() -> &'static [Tool] { &SHAPE_TOOLS } diff --git a/src/ui/toolbar/snapshot/build.rs b/src/ui/toolbar/snapshot/build.rs index 325be6c4..6499d5c2 100644 --- a/src/ui/toolbar/snapshot/build.rs +++ b/src/ui/toolbar/snapshot/build.rs @@ -101,6 +101,8 @@ impl ToolbarSnapshot { .collect(); let drawer_open = state.toolbar_drawer_open; let drawer_tab = state.toolbar_drawer_tab; + let customize_items_open = state.toolbar_customize_items_open; + let customize_items_group = state.toolbar_customize_items_group; let show_actions_advanced = state.show_actions_advanced; let show_zoom_actions = state.show_zoom_actions; let show_pages_section = state.show_pages_section; @@ -158,6 +160,7 @@ impl ToolbarSnapshot { use_icons: state.toolbar_use_icons, toolbar_scale: state.toolbar_scale, layout_mode: state.toolbar_layout_mode, + resolved_toolbar_items: state.resolved_toolbar_items.clone(), show_more_colors: state.show_more_colors, show_actions_section: state.show_actions_section, show_actions_advanced, @@ -184,6 +187,8 @@ impl ToolbarSnapshot { shape_picker_open: state.toolbar_shapes_expanded, drawer_open, drawer_tab, + customize_items_open, + customize_items_group, binding_hints, show_drawer_hint, is_transparent: state.board_is_transparent(), diff --git a/src/ui/toolbar/snapshot/types.rs b/src/ui/toolbar/snapshot/types.rs index 1cf53780..262f94fe 100644 --- a/src/ui/toolbar/snapshot/types.rs +++ b/src/ui/toolbar/snapshot/types.rs @@ -1,4 +1,4 @@ -use crate::config::ToolbarLayoutMode; +use crate::config::{ResolvedToolbarItems, ToolbarGroupId, ToolbarItemId, ToolbarLayoutMode}; use crate::draw::{Color, EraserKind, FontDescriptor}; use crate::input::state::PresetFeedbackKind; use crate::input::tool::{ToolControlGroup, ToolProfile}; @@ -271,6 +271,8 @@ pub struct ToolbarSnapshot { pub toolbar_scale: f64, /// Current toolbar layout mode pub layout_mode: ToolbarLayoutMode, + /// Resolved known item-level toolbar visibility config. + pub resolved_toolbar_items: ResolvedToolbarItems, /// Whether to show extended color palette pub show_more_colors: bool, /// Whether to show the Actions section @@ -310,6 +312,10 @@ pub struct ToolbarSnapshot { pub drawer_open: bool, /// Active drawer tab pub drawer_tab: ToolbarDrawerTab, + /// Whether the Settings drawer is showing the toolbar item customization sub-panel. + pub customize_items_open: bool, + /// Selected toolbar item customization group in the Settings drawer sub-panel. + pub customize_items_group: Option, /// Number of preset slots to display pub preset_slot_count: usize, /// Preset slot previews @@ -337,6 +343,57 @@ pub struct ToolbarSnapshot { } impl ToolbarSnapshot { + pub fn toolbar_item_hidden(&self, item: ToolbarItemId) -> bool { + self.resolved_toolbar_items.is_hidden(item) + } + + pub fn toolbar_group_hidden(&self, group: ToolbarGroupId) -> bool { + self.toolbar_item_hidden(group.toolbar_item_id()) + } + + pub fn side_section_hidden(&self, section: ToolbarSideSection) -> bool { + let group = match section { + ToolbarSideSection::Colors => ToolbarGroupId::Colors, + ToolbarSideSection::Presets => ToolbarGroupId::Presets, + ToolbarSideSection::Thickness => ToolbarGroupId::Thickness, + ToolbarSideSection::EraserMode => ToolbarGroupId::EraserMode, + ToolbarSideSection::PolygonSides => ToolbarGroupId::PolygonSides, + ToolbarSideSection::ArrowLabels => ToolbarGroupId::ArrowLabels, + ToolbarSideSection::StepMarkers => ToolbarGroupId::StepMarkers, + ToolbarSideSection::MarkerOpacity => ToolbarGroupId::MarkerOpacity, + ToolbarSideSection::TextSize => ToolbarGroupId::TextSize, + ToolbarSideSection::Font => ToolbarGroupId::Font, + ToolbarSideSection::Actions => ToolbarGroupId::Actions, + ToolbarSideSection::Boards => ToolbarGroupId::Boards, + ToolbarSideSection::Pages => ToolbarGroupId::Pages, + ToolbarSideSection::StepUndo => ToolbarGroupId::StepUndo, + ToolbarSideSection::Session => ToolbarGroupId::Session, + 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::Presets + | ToolbarSideSection::Actions + | ToolbarSideSection::Boards + | ToolbarSideSection::Pages + | ToolbarSideSection::StepUndo + | ToolbarSideSection::Session + | ToolbarSideSection::Settings => None, + }; + + self.toolbar_group_hidden(group) + || legacy_item + .is_some_and(|item| self.toolbar_item_hidden(ToolbarItemId::from_known(item))) + } + pub fn side_section_collapsed(&self, section: ToolbarSideSection) -> bool { self.collapsed_side_sections.contains(§ion) }