Skip to content

Commit db2ab6f

Browse files
authored
Merge pull request #253 from devmobasa/feat/configurable-toolbar-ordering
feat(toolbar): add configurable item ordering
2 parents 0a05375 + 5d98dc9 commit db2ab6f

44 files changed

Lines changed: 2525 additions & 1048 deletions

File tree

Some content is hidden

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

config.example.toml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,46 @@ hidden = [
427427
"top.utility.screenshot",
428428
]
429429

430+
[ui.toolbar.items.order]
431+
# Optional order overrides. Empty lists use the built-in order.
432+
# Known IDs omitted from a non-empty list are appended in the default order.
433+
# Side section ordering uses runtime block representatives; detailed sections
434+
# such as eraser-mode, polygon-sides, and font remain visibility-only IDs.
435+
top_tools = [
436+
"top.tool.select",
437+
"top.tool.pen",
438+
"top.tool.marker",
439+
"top.tool.step-marker",
440+
"top.tool.eraser",
441+
"top.tool.line",
442+
"top.tool.rect",
443+
"top.tool.ellipse",
444+
"top.tool.arrow",
445+
"top.tool.blur",
446+
]
447+
top_controls = [
448+
"top.utility.text",
449+
"top.utility.sticky-note",
450+
"top.utility.screenshot",
451+
"top.utility.clear-canvas",
452+
"top.utility.highlight",
453+
]
454+
side_sections = [
455+
"side.group.colors",
456+
"side.group.presets",
457+
"side.group.thickness",
458+
"side.group.arrow-labels",
459+
"side.group.step-markers",
460+
"side.group.marker-opacity",
461+
"side.group.text-size",
462+
"side.group.actions",
463+
"side.group.boards",
464+
"side.group.pages",
465+
"side.group.step-undo",
466+
"side.group.session",
467+
"side.group.settings",
468+
]
469+
430470
# ───────────────────────────────────────────────────────────────────────────────
431471
# Status Bar Styling
432472
# ───────────────────────────────────────────────────────────────────────────────

configurator/src/app/search/tests.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::path::PathBuf;
55

66
use crate::models::session::SessionArtifactSummary;
77
use crate::models::{KeybindingsTabId, SearchQuery, SessionCatalogItem, TabId, UiTabId};
8+
use wayscriber::config::toolbar_item_ids as ids;
89

910
#[test]
1011
fn active_search_tab_click_corrects_keybindings_nested_tab() {
@@ -289,8 +290,8 @@ fn parent_scoped_ui_queries_match_concrete_nested_tabs() {
289290
fn ui_nested_visible_control_labels_match_concrete_nested_tabs() {
290291
let cases = [
291292
("layout mode", UiTabId::Toolbar),
292-
("top.tool.blur", UiTabId::ToolbarVisibility),
293-
("side.group.presets", UiTabId::ToolbarVisibility),
293+
(ids::TOP_TOOL_BLUR.as_str(), UiTabId::ToolbarVisibility),
294+
(ids::SIDE_GROUP_PRESETS.as_str(), UiTabId::ToolbarVisibility),
294295
("status bar position", UiTabId::StatusBar),
295296
("click highlight radius", UiTabId::ClickHighlight),
296297
];

configurator/src/app/update/fields.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use iced::Task;
2+
use wayscriber::config::{ToolbarItemId, ToolbarItemOrderGroup};
23

34
use crate::messages::Message;
45
use crate::models::{
@@ -12,7 +13,6 @@ use crate::models::{
1213
};
1314
#[cfg(feature = "tablet-input")]
1415
use crate::models::{PressureThicknessEditModeOption, PressureThicknessEntryModeOption};
15-
use wayscriber::config::ToolbarItemId;
1616

1717
use super::super::state::{ConfiguratorApp, StatusMessage};
1818

@@ -180,6 +180,28 @@ impl ConfiguratorApp {
180180
Task::none()
181181
}
182182

183+
pub(super) fn handle_toolbar_item_move_requested(
184+
&mut self,
185+
group: ToolbarItemOrderGroup,
186+
id: ToolbarItemId,
187+
delta: isize,
188+
) -> Task<Message> {
189+
self.status = StatusMessage::idle();
190+
self.draft.move_toolbar_item(group, id, delta);
191+
self.refresh_dirty_flag();
192+
Task::none()
193+
}
194+
195+
pub(super) fn handle_toolbar_item_order_reset(
196+
&mut self,
197+
group: ToolbarItemOrderGroup,
198+
) -> Task<Message> {
199+
self.status = StatusMessage::idle();
200+
self.draft.reset_toolbar_item_order(group);
201+
self.refresh_dirty_flag();
202+
Task::none()
203+
}
204+
183205
pub(super) fn handle_session_storage_mode_changed(
184206
&mut self,
185207
option: SessionStorageModeOption,

configurator/src/app/update/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ impl ConfiguratorApp {
122122
Message::ToolbarItemVisibilityChanged(id, visible) => {
123123
self.handle_toolbar_item_visibility_changed(id, visible)
124124
}
125+
Message::ToolbarItemMoveRequested(group, id, delta) => {
126+
self.handle_toolbar_item_move_requested(group, id, delta)
127+
}
128+
Message::ToolbarItemOrderReset(group) => self.handle_toolbar_item_order_reset(group),
125129
Message::BoardsAddItem => self.handle_boards_add_item(),
126130
Message::BoardsRemoveItem(index) => self.handle_boards_remove_item(index),
127131
Message::BoardsMoveItemUp(index) => self.handle_boards_move_item(index, true),

configurator/src/app/view/ui/toolbar.rs

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use iced::widget::{checkbox, column, pick_list, row, scrollable, text};
1+
use std::collections::BTreeSet;
2+
3+
use iced::widget::{button, checkbox, column, pick_list, row, scrollable, text};
24
use iced::{Element, Length};
35
use wayscriber::config::{
4-
ResolvedToolbarItems, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemSurface,
5-
ToolbarItemsConfig, toolbar_item_definitions,
6+
ResolvedToolbarItems, ToolbarItemCategory, ToolbarItemDefinition, ToolbarItemOrderGroup,
7+
ToolbarItemSurface, ToolbarItemsConfig, toolbar_item_definitions, toolbar_item_order_group,
68
};
79

810
use crate::app::scroll::CONTENT_SCROLL_ID;
@@ -255,7 +257,7 @@ fn toolbar_item_visibility_section<'a>(
255257
);
256258
}
257259

258-
for definition in toolbar_item_definitions() {
260+
for definition in toolbar_item_definitions_for_display(&resolved) {
259261
if current_surface != Some(definition.surface) {
260262
current_surface = Some(definition.surface);
261263
current_category = None;
@@ -287,17 +289,81 @@ fn toolbar_item_visibility_row<'a>(
287289
"default: {}",
288290
visibility_override_label(!defaults.is_hidden(id))
289291
);
292+
let order_group = configurator_order_group(definition);
293+
let order = order_group.and_then(|group| {
294+
let index = resolved.order.index_of(group, id)?;
295+
let len = resolved.order.ordered_ids(group).len();
296+
Some((group, index, index > 0, index + 1 < len))
297+
});
290298

291-
row![
299+
let mut cells = row![
292300
checkbox(visible)
293301
.label(definition.label)
294302
.on_toggle(move |value| Message::ToolbarItemVisibilityChanged(id, value)),
295303
text(definition.id.as_str()).size(12).width(Length::Fill),
296-
text(default).size(12),
297304
]
298305
.spacing(12)
299-
.align_y(iced::Alignment::Center)
300-
.into()
306+
.align_y(iced::Alignment::Center);
307+
if let Some((group, _, can_move_up, can_move_down)) = order {
308+
let up = if can_move_up {
309+
button(text("^")).on_press(Message::ToolbarItemMoveRequested(group, id, -1))
310+
} else {
311+
button(text("^"))
312+
};
313+
let down = if can_move_down {
314+
button(text("v")).on_press(Message::ToolbarItemMoveRequested(group, id, 1))
315+
} else {
316+
button(text("v"))
317+
};
318+
cells = cells
319+
.push(up)
320+
.push(down)
321+
.push(button(text("Reset")).on_press(Message::ToolbarItemOrderReset(group)));
322+
}
323+
cells.push(text(default).size(12)).into()
324+
}
325+
326+
fn toolbar_item_definitions_for_display(
327+
resolved: &ResolvedToolbarItems,
328+
) -> Vec<&'static ToolbarItemDefinition> {
329+
let mut result = Vec::new();
330+
let mut emitted = BTreeSet::new();
331+
let mut emitted_groups = BTreeSet::new();
332+
333+
for definition in toolbar_item_definitions() {
334+
if let Some(group) = configurator_order_group(definition) {
335+
if emitted_groups.insert(group) {
336+
for id in resolved.order.ordered_ids(group) {
337+
if let Some(ordered_definition) = toolbar_item_definitions()
338+
.iter()
339+
.find(|candidate| candidate.id == *id)
340+
&& configurator_order_group(ordered_definition) == Some(group)
341+
{
342+
result.push(ordered_definition);
343+
emitted.insert(ordered_definition.id);
344+
}
345+
}
346+
}
347+
if emitted.contains(&definition.id) {
348+
continue;
349+
}
350+
}
351+
result.push(definition);
352+
emitted.insert(definition.id);
353+
}
354+
355+
result
356+
}
357+
358+
fn configurator_order_group(definition: &ToolbarItemDefinition) -> Option<ToolbarItemOrderGroup> {
359+
let group = toolbar_item_order_group(definition)?;
360+
matches!(
361+
group,
362+
ToolbarItemOrderGroup::TopTools
363+
| ToolbarItemOrderGroup::TopControls
364+
| ToolbarItemOrderGroup::SideSections
365+
)
366+
.then_some(group)
301367
}
302368

303369
fn visibility_override_label(visible: bool) -> &'static str {

configurator/src/messages.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::PathBuf;
22
use std::sync::Arc;
33

4-
use wayscriber::config::{Config, ToolbarItemId};
4+
use wayscriber::config::{Config, ToolbarItemId, ToolbarItemOrderGroup};
55

66
use crate::models::{
77
BoardBackgroundOption, BoardItemTextField, BoardItemToggleField, ColorMode, ColorPickerId,
@@ -74,6 +74,8 @@ pub enum Message {
7474
ToolbarOverrideModeChanged(ToolbarLayoutModeOption),
7575
ToolbarOverrideChanged(ToolbarOverrideField, OverrideOption),
7676
ToolbarItemVisibilityChanged(ToolbarItemId, bool),
77+
ToolbarItemMoveRequested(ToolbarItemOrderGroup, ToolbarItemId, isize),
78+
ToolbarItemOrderReset(ToolbarItemOrderGroup),
7779
BoardsAddItem,
7880
BoardsRemoveItem(usize),
7981
BoardsMoveItemUp(usize),

configurator/src/models/config/setters.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::super::fields::{
44
ToolbarOverrideField, TripletField,
55
};
66
use super::draft::ConfigDraft;
7-
use wayscriber::config::ToolbarItemId;
7+
use wayscriber::config::{ToolbarItemId, ToolbarItemOrderGroup};
88

99
impl ConfigDraft {
1010
pub fn apply_toolbar_layout_mode(&mut self, mode: ToolbarLayoutModeOption) {
@@ -36,6 +36,19 @@ impl ConfigDraft {
3636
self.ui_toolbar_items.set_hidden(id, !visible);
3737
}
3838

39+
pub fn move_toolbar_item(
40+
&mut self,
41+
group: ToolbarItemOrderGroup,
42+
id: ToolbarItemId,
43+
delta: isize,
44+
) {
45+
self.ui_toolbar_items.move_item_by(group, id, delta);
46+
}
47+
48+
pub fn reset_toolbar_item_order(&mut self, group: ToolbarItemOrderGroup) {
49+
self.ui_toolbar_items.reset_known_order_to_defaults(group);
50+
}
51+
3952
pub fn set_mouse_drag_tool(
4053
&mut self,
4154
button: DragMouseButton,

configurator/src/models/config/tests.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use super::{ConfigDraft, RenderProfileSelectionOption};
1010
use wayscriber::config::{
1111
ColorSpec, Config, PdfFitMode, PdfLabelContentMode, PdfLabelPosition, PdfOrientation,
1212
PdfPageSize, PdfTransparentBackground, PresetToolStatesConfig, RenderColorMappingConfig,
13-
RenderProfileConfig, RenderProfileExportMode, ToolPresetConfig, ToolbarItemsConfig,
14-
XdgFocusLossBehavior,
13+
RenderProfileConfig, RenderProfileExportMode, ToolPresetConfig, ToolbarItemOrderConfig,
14+
ToolbarItemOrderGroup, ToolbarItemsConfig, XdgFocusLossBehavior, toolbar_item_ids as ids,
1515
};
1616
use wayscriber::input::{DragTool, PerToolDrawingSettings, Tool};
1717

@@ -69,22 +69,53 @@ fn config_draft_round_trips_toolbar_item_visibility_preserving_unknown_ids() {
6969
config.ui.toolbar.items = ToolbarItemsConfig {
7070
hidden: vec![
7171
"future.toolbar.item".to_string(),
72-
"side.actions.undo-all".to_string(),
73-
"side.actions.undo-all".to_string(),
72+
ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(),
73+
ids::SIDE_ACTIONS_UNDO_ALL.as_str().to_string(),
7474
],
75+
order: ToolbarItemOrderConfig::default(),
7576
};
7677

7778
let mut draft = ConfigDraft::from_config(&config);
78-
draft.set_toolbar_item_visible("side.actions.undo-all".parse().expect("known id"), true);
79-
draft.set_toolbar_item_visible("top.tool.pen".parse().expect("known id"), false);
79+
draft.set_toolbar_item_visible(ids::SIDE_ACTIONS_UNDO_ALL, true);
80+
draft.set_toolbar_item_visible(ids::TOP_TOOL_PEN, false);
8081

8182
let round_trip = draft
8283
.to_config(&config)
8384
.expect("expected config to round trip");
8485

8586
assert_eq!(
8687
round_trip.ui.toolbar.items.hidden,
87-
vec!["future.toolbar.item", "top.tool.pen"]
88+
vec![
89+
"future.toolbar.item".to_string(),
90+
ids::TOP_TOOL_PEN.as_str().to_string()
91+
]
92+
);
93+
}
94+
95+
#[test]
96+
fn config_draft_round_trips_toolbar_item_order_preserving_unknown_ids() {
97+
let mut config = Config::default();
98+
config.ui.toolbar.items.order.top_tools = vec![
99+
"future.toolbar.item".to_string(),
100+
ids::TOP_TOOL_PEN.as_str().to_string(),
101+
ids::TOP_TOOL_SELECT.as_str().to_string(),
102+
];
103+
104+
let mut draft = ConfigDraft::from_config(&config);
105+
draft.move_toolbar_item(ToolbarItemOrderGroup::TopTools, ids::TOP_TOOL_PEN, 1);
106+
107+
let round_trip = draft
108+
.to_config(&config)
109+
.expect("expected config to round trip");
110+
111+
assert!(
112+
round_trip
113+
.ui
114+
.toolbar
115+
.items
116+
.order
117+
.top_tools
118+
.contains(&"future.toolbar.item".to_string())
88119
);
89120
}
90121

docs/CONFIG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,35 @@ hidden = [
566566
"side.actions.undo-all",
567567
"side.group.presets",
568568
]
569+
570+
[ui.toolbar.items.order]
571+
# Optional order overrides. Empty lists use the built-in order.
572+
# Known IDs omitted from a non-empty list append in the default order.
573+
# Side section ordering uses runtime block representatives; detailed sections
574+
# such as eraser-mode, polygon-sides, and font remain visibility-only IDs.
575+
top_tools = [
576+
"top.tool.select",
577+
"top.tool.pen",
578+
"top.tool.marker",
579+
"top.tool.step-marker",
580+
"top.tool.eraser",
581+
]
582+
top_controls = [
583+
"top.utility.text",
584+
"top.utility.sticky-note",
585+
"top.utility.screenshot",
586+
"top.utility.clear-canvas",
587+
"top.utility.highlight",
588+
]
589+
side_sections = [
590+
"side.group.colors",
591+
"side.group.presets",
592+
"side.group.thickness",
593+
"side.group.actions",
594+
"side.group.pages",
595+
"side.group.boards",
596+
"side.group.settings",
597+
]
569598
```
570599

571600
**Behavior:**
@@ -591,6 +620,8 @@ hidden = [
591620
- **Force inline**: `force_inline` (or `WAYSCRIBER_FORCE_INLINE_TOOLBARS`) skips layer-shell toolbars.
592621
- **Pinned**: `top_pinned`/`side_pinned` control whether each toolbar opens on startup.
593622
- **Hidden items**: `ui.toolbar.items.hidden` removes known toolbar buttons/sections from sizing, drawing, and hit testing while preserving unknown future IDs.
623+
- **Item order**: `ui.toolbar.items.order.top_tools`, `top_controls`, and `side_sections` reorder supported toolbar items. `side_sections` orders runtime block representatives; `side.group.eraser-mode`, `side.group.polygon-sides`, and `side.group.font` can be hidden individually but are not independently orderable. Unknown future IDs and wrong-group IDs are ignored at runtime but preserved across saves.
624+
- **Live customization**: the overlay Customize tab supports show/hide, move up/down, and drag reorder for supported groups. The configurator supports the same saved order with up/down controls.
594625
- **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.
595626

596627
**Defaults:** all set as above.

0 commit comments

Comments
 (0)