diff --git a/assets/icons/ui/refresh.svg b/assets/icons/ui/refresh.svg
new file mode 100644
index 000000000..a7da370a7
--- /dev/null
+++ b/assets/icons/ui/refresh.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/ui/swap.svg b/assets/icons/ui/swap.svg
new file mode 100644
index 000000000..28b9a6395
--- /dev/null
+++ b/assets/icons/ui/swap.svg
@@ -0,0 +1 @@
+
diff --git a/src/ui/icons.rs b/src/ui/icons.rs
index 7a2385fb4..b3d60b075 100644
--- a/src/ui/icons.rs
+++ b/src/ui/icons.rs
@@ -59,6 +59,8 @@ static LAY_ULK: &[u8] = include_bytes!("../../assets/icons/layers/layulk.svg");
pub static CHECK: &[u8] = include_bytes!("../../assets/icons/ui/check.svg");
pub static CLOSE: &[u8] = include_bytes!("../../assets/icons/ui/close.svg");
pub static PLUS: &[u8] = include_bytes!("../../assets/icons/ui/plus.svg");
+pub static REFRESH: &[u8] = include_bytes!("../../assets/icons/ui/refresh.svg");
+pub static SWAP: &[u8] = include_bytes!("../../assets/icons/ui/swap.svg");
pub static MINUS: &[u8] = include_bytes!("../../assets/icons/ui/minus.svg");
pub static TRASH: &[u8] = include_bytes!("../../assets/icons/ui/trash.svg");
pub static NODE_GRAPH: &[u8] = include_bytes!("../../assets/icons/ui/node_graph.svg");
diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs
index fcd11afca..7497baed3 100644
--- a/src/ui/window/mod.rs
+++ b/src/ui/window/mod.rs
@@ -27,7 +27,6 @@ pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
pub mod pc_manager;
-pub mod row_menu;
pub mod sheet_set;
pub mod pdf_dialogs;
pub mod xref_attach;
diff --git a/src/ui/window/row_menu.rs b/src/ui/window/row_menu.rs
deleted file mode 100644
index 4f3f5a121..000000000
--- a/src/ui/window/row_menu.rs
+++ /dev/null
@@ -1,288 +0,0 @@
-//! A right-click menu for palette rows with one level of fly-out submenus.
-//!
-//! The menu opens at the cursor (flipped at the window edges); an open fly-out
-//! is laid out beside it — on the right when it fits, otherwise on the left —
-//! so the menu itself never moves. Pressing a submenu entry keeps the menu open.
-
-use iced::advanced::layout::{self, Layout};
-use iced::advanced::widget::{self, tree, Tree, Widget};
-use iced::advanced::{mouse, overlay, renderer, Shell};
-use iced::{keyboard, Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
-
-use crate::app::Message;
-
-const FLYOUT_GAP: f32 = 2.0;
-
-pub(crate) struct RowMenu<'a> {
- underlay: Element<'a, Message>,
- main: Element<'a, Message>,
- /// Per child of the menu's column: a submenu entry or a separator (a click on it keeps the menu open).
- keep_open: Vec,
- /// The open fly-out: its entry's index in the column, and its content.
- flyout: Option<(usize, Element<'a, Message>)>,
- /// Published when the menu closes with a fly-out open.
- on_close: Message,
-}
-
-#[derive(Default)]
-struct State {
- show: bool,
- position: Point,
-}
-
-impl<'a> RowMenu<'a> {
- pub(crate) fn new(
- underlay: impl Into>,
- main: Element<'a, Message>,
- keep_open: Vec,
- flyout: Option<(usize, Element<'a, Message>)>,
- on_close: Message,
- ) -> Self {
- Self { underlay: underlay.into(), main, keep_open, flyout, on_close }
- }
-}
-
-impl<'a> Widget for RowMenu<'a> {
- fn size(&self) -> Size {
- self.underlay.as_widget().size()
- }
-
- fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
- self.underlay.as_widget_mut().layout(&mut tree.children[0], renderer, limits)
- }
-
- fn draw(
- &self,
- tree: &Tree,
- renderer: &mut Renderer,
- theme: &Theme,
- style: &renderer::Style,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- viewport: &Rectangle,
- ) {
- self.underlay.as_widget().draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport);
- }
-
- fn tag(&self) -> tree::Tag {
- tree::Tag::of::()
- }
-
- fn state(&self) -> tree::State {
- tree::State::new(State::default())
- }
-
- fn diff(&mut self, tree: &mut Tree) {
- let mut refs: Vec<&mut Element<'a, Message>> = vec![&mut self.underlay, &mut self.main];
- if let Some((_, f)) = &mut self.flyout {
- refs.push(f);
- }
- tree.diff_children(&mut refs);
- }
-
- fn update(
- &mut self,
- tree: &mut Tree,
- event: &Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- renderer: &Renderer,
- shell: &mut Shell<'_, Message>,
- viewport: &Rectangle,
- ) {
- if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event {
- if let Some(p) = cursor.position_over(layout.bounds()) {
- let state: &mut State = tree.state.downcast_mut();
- state.show = true;
- state.position = p;
- if self.flyout.is_some() {
- shell.publish(self.on_close.clone());
- }
- shell.capture_event();
- shell.request_redraw();
- return;
- }
- }
- self.underlay
- .as_widget_mut()
- .update(&mut tree.children[0], event, layout, cursor, renderer, shell, viewport);
- }
-
- fn mouse_interaction(
- &self,
- tree: &Tree,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- viewport: &Rectangle,
- renderer: &Renderer,
- ) -> mouse::Interaction {
- self.underlay.as_widget().mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
- }
-
- fn operate(
- &mut self,
- tree: &mut Tree,
- layout: Layout<'_>,
- renderer: &Renderer,
- operation: &mut dyn widget::Operation,
- ) {
- self.underlay.as_widget_mut().operate(&mut tree.children[0], layout, renderer, operation);
- }
-
- fn overlay<'b>(
- &'b mut self,
- tree: &'b mut Tree,
- layout: Layout<'b>,
- renderer: &Renderer,
- viewport: &Rectangle,
- translation: Vector,
- ) -> Option> {
- let state: &mut State = tree.state.downcast_mut();
- if !state.show {
- return self.underlay.as_widget_mut().overlay(&mut tree.children[0], layout, renderer, viewport, translation);
- }
- let position = state.position + translation;
- let (_, trees) = tree.children.split_at_mut(1);
- let (main_tree, fly_tree) = trees.split_at_mut(1);
- Some(overlay::Element::new(Box::new(MenuOverlay {
- state,
- position,
- main: &mut self.main,
- main_tree: &mut main_tree[0],
- keep_open: &self.keep_open,
- flyout: self.flyout.as_mut().map(|(i, f)| (*i, f)).zip(fly_tree.first_mut()),
- on_close: &self.on_close,
- })))
- }
-}
-
-impl<'a> From> for Element<'a, Message> {
- fn from(w: RowMenu<'a>) -> Self {
- Element::new(w)
- }
-}
-
-struct MenuOverlay<'a, 'b> {
- state: &'b mut State,
- position: Point,
- main: &'b mut Element<'a, Message>,
- main_tree: &'b mut Tree,
- keep_open: &'b [bool],
- flyout: Option<((usize, &'b mut Element<'a, Message>), &'b mut Tree)>,
- on_close: &'b Message,
-}
-
-/// The entries of a menu box (container › column › entries).
-fn entries(main: Layout<'_>) -> impl Iterator- > {
- main.children().next().into_iter().flat_map(|column| column.children())
-}
-
-impl MenuOverlay<'_, '_> {
- fn close(&mut self, shell: &mut Shell<'_, Message>) {
- self.state.show = false;
- if self.flyout.is_some() {
- shell.publish(self.on_close.clone());
- }
- shell.request_redraw();
- }
-}
-
-impl overlay::Overlay for MenuOverlay<'_, '_> {
- fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
- let limits = layout::Limits::new(Size::ZERO, bounds);
- let main = self.main.as_widget_mut().layout(self.main_tree, renderer, &limits);
- let size = main.size();
- // At the cursor, flipped to the other side at the window edges.
- let mut p = self.position;
- if p.x + size.width > bounds.width {
- p.x = (p.x - size.width).max(0.0);
- }
- if p.y + size.height > bounds.height {
- p.y = (p.y - size.height).max(0.0);
- }
- let main = main.move_to(p);
- let mut nodes = Vec::with_capacity(2);
- if let Some(((index, flyout), tree)) = self.flyout.as_mut() {
- let node = flyout.as_widget_mut().layout(tree, renderer, &limits);
- let fly = node.size();
- // Its first item level with the entry (both boxes have the same inset).
- let entry_y = main.children().first().and_then(|column| column.children().get(*index)).map_or(0.0, |e| e.bounds().y);
- let y = (p.y + entry_y).min(bounds.height - fly.height).max(0.0);
- // On the right when it fits, otherwise on the left.
- let right = p.x + size.width + FLYOUT_GAP;
- let x = if right + fly.width <= bounds.width { right } else { (p.x - fly.width - FLYOUT_GAP).max(0.0) };
- nodes.push(main);
- nodes.push(node.move_to(Point::new(x, y)));
- } else {
- nodes.push(main);
- }
- layout::Node::with_children(bounds, nodes)
- }
-
- fn draw(&self, renderer: &mut Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor) {
- let mut children = layout.children();
- if let Some(main) = children.next() {
- self.main.as_widget().draw(self.main_tree, renderer, theme, style, main, cursor, &layout.bounds());
- }
- if let (Some(((_, flyout), tree)), Some(fly)) = (self.flyout.as_ref(), children.next()) {
- flyout.as_widget().draw(tree, renderer, theme, style, fly, cursor, &layout.bounds());
- }
- }
-
- fn update(
- &mut self,
- event: &Event,
- layout: Layout<'_>,
- cursor: mouse::Cursor,
- renderer: &Renderer,
- shell: &mut Shell<'_, Message>,
- ) {
- let mut children = layout.children();
- let Some(main) = children.next() else { return };
- let fly = children.next();
- let over = cursor.is_over(main.bounds()) || fly.is_some_and(|f| cursor.is_over(f.bounds()));
- match event {
- Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Escape), .. }) => {
- self.close(shell);
- shell.capture_event();
- return;
- }
- Event::Mouse(mouse::Event::ButtonPressed(_)) if !over => {
- self.close(shell);
- return;
- }
- _ => {}
- }
- let viewport = layout.bounds();
- self.main.as_widget_mut().update(self.main_tree, event, main, cursor, renderer, shell, &viewport);
- if let (Some(((_, flyout), tree)), Some(fly)) = (self.flyout.as_mut(), fly) {
- flyout.as_widget_mut().update(tree, event, fly, cursor, renderer, shell, &viewport);
- }
- if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) = event {
- // Only a chosen entry closes the menu: a submenu entry opens its
- // fly-out, and separators, gaps and padding do nothing.
- let chosen = match entries(main).zip(self.keep_open.iter()).find(|(e, _)| cursor.is_over(e.bounds())) {
- Some((_, keep)) => !keep,
- None => fly.is_some_and(|f| entries(f).any(|e| cursor.is_over(e.bounds()))),
- };
- if chosen {
- self.close(shell);
- }
- }
- if over {
- shell.capture_event();
- }
- }
-
- fn mouse_interaction(&self, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer) -> mouse::Interaction {
- let mut children = layout.children();
- let main = children.next().map(|m| {
- self.main.as_widget().mouse_interaction(self.main_tree, m, cursor, &m.bounds(), renderer)
- });
- let fly = match (self.flyout.as_ref(), children.next()) {
- (Some(((_, f), tree)), Some(l)) => Some(f.as_widget().mouse_interaction(tree, l, cursor, &l.bounds(), renderer)),
- _ => None,
- };
- fly.filter(|i| *i != mouse::Interaction::default()).or(main).unwrap_or_default()
- }
-}
diff --git a/src/ui/window/sheet_set.rs b/src/ui/window/sheet_set.rs
index ad4bfa780..1bf1c0d2a 100644
--- a/src/ui/window/sheet_set.rs
+++ b/src/ui/window/sheet_set.rs
@@ -672,8 +672,7 @@ fn plain_row<'a>(
if menu.is_empty() {
return area.into();
}
- let (main, keep_open, flyout) = menu_panel(&menu, open_sub.as_deref());
- crate::ui::window::row_menu::RowMenu::new(area, main, keep_open, flyout, msg(SheetSetMsg::Submenu(None))).into()
+ iced_aw::ContextMenu::new(area, move || menu_panel(&menu, open_sub.as_deref())).into()
}
fn menu_box<'b>(items: Vec>) -> Element<'b, Message> {
@@ -688,32 +687,34 @@ fn menu_box<'b>(items: Vec>) -> Element<'b, Message> {
.into()
}
-/// A row menu: the menu box, which children keep it open when clicked (submenu entries, separators), and the
-/// open fly-out (its entry index and items). Hovering, pressing or right
-/// pressing a submenu entry opens its fly-out.
-fn menu_panel(menu: &[Mi], open: Option<&str>) -> (Element<'static, Message>, Vec, Option<(usize, Element<'static, Message>)>) {
+/// A row's right-click menu. A submenu expands in place below its entry
+/// while hovered (a click would close the menu).
+fn menu_panel(menu: &[Mi], open: Option<&str>) -> Element<'static, Message> {
let mut items: Vec> = Vec::new();
- let mut keep_open = Vec::new();
- let mut flyout = None;
for item in menu {
match item {
Mi::Sep => items.push(menu_separator()),
- Mi::Entry(label, m) => {
- items.push(mouse_area(menu_entry(label.clone(), m.clone())).on_enter(msg(SheetSetMsg::Submenu(None))).into());
- }
+ Mi::Entry(label, m) => items.push(menu_entry(label.clone(), m.clone())),
Mi::Sub(label, key, sub) => {
- let open_msg = msg(SheetSetMsg::Submenu(Some(key.clone())));
- let entry = menu_entry(format!("{label} ▸"), Some(open_msg.clone()));
- if open == Some(key.as_str()) {
- let subs = sub.iter().map(|(l, m)| menu_entry(l.clone(), m.clone())).collect();
- flyout = Some((items.len(), menu_box(subs)));
+ let expanded = open == Some(key.as_str());
+ let arrow = if expanded {
+ crate::ui::icons::themed_arrow_down(9.0)
+ } else {
+ crate::ui::icons::themed_arrow_right(9.0)
+ };
+ let header = container(row![text(label.clone()).size(12).width(Fill), arrow].align_y(iced::Center))
+ .width(Fill)
+ .padding([5, 12]);
+ items.push(mouse_area(header).on_enter(msg(SheetSetMsg::Submenu(Some(key.clone())))).into());
+ if expanded {
+ for (l, m) in sub {
+ items.push(container(menu_entry(l.clone(), m.clone())).padding(iced::Padding::ZERO.left(12)).into());
+ }
}
- items.push(mouse_area(entry).on_enter(open_msg.clone()).on_right_press(open_msg).into());
}
}
- keep_open.push(matches!(item, Mi::Sub(..) | Mi::Sep));
}
- (menu_box(items), keep_open, flyout)
+ menu_box(items)
}
/// A menu item: translated label and message (an empty label is a separator).
@@ -976,7 +977,7 @@ pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) ->
}
let header = row![
heading("Sheets"),
- button(text("↻").size(13))
+ button(crate::ui::icons::themed(crate::ui::icons::REFRESH, 13.0))
.on_press(msg(SheetSetMsg::Refresh))
.style(button::subtle)
.padding([2, 4]),
@@ -990,8 +991,15 @@ pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) ->
let by = state.by_category;
let header = row![
heading(if by { "View by category" } else { "View by sheet" }),
- button(text(format!("{} ⇄", t!(if by { "View by sheet" } else { "View by category" }))).size(11))
- .on_press(msg(SheetSetMsg::ViewsByCategory(!by)))
+ button(
+ row![
+ text(t!(if by { "View by sheet" } else { "View by category" }).into_owned()).size(11),
+ crate::ui::icons::themed(crate::ui::icons::SWAP, 11.0),
+ ]
+ .spacing(4)
+ .align_y(iced::Center),
+ )
+ .on_press(msg(SheetSetMsg::ViewsByCategory(!by)))
.style(button::subtle)
.padding([2, 4]),
button(crate::ui::icons::semantic(crate::ui::icons::PLUS, 12.0))
@@ -1008,7 +1016,10 @@ pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) ->
(Some(_), SsmTab::ModelViews) => {
let header = row![
heading("Locations"),
- button(text("↻").size(13)).on_press(msg(SheetSetMsg::Refresh)).style(button::subtle).padding([2, 4]),
+ button(crate::ui::icons::themed(crate::ui::icons::REFRESH, 13.0))
+ .on_press(msg(SheetSetMsg::Refresh))
+ .style(button::subtle)
+ .padding([2, 4]),
button(crate::ui::icons::semantic(crate::ui::icons::PLUS, 12.0))
.on_press(msg(SheetSetMsg::AddLocation))
.style(button::subtle)
@@ -1168,7 +1179,7 @@ fn wizard_view<'a>(w: &'a Wizard) -> Element<'a, Message> {
let folders = w.folders.iter().enumerate().map(|(i, f)| {
row![
text(f.clone()).size(12).width(Fill),
- button(text("×").size(12))
+ button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::RemoveFolder(i))))
.style(button::text)
.padding([0, 6]),
@@ -1228,12 +1239,12 @@ fn wizard_view<'a>(w: &'a Wizard) -> Element<'a, Message> {
(1, _) => Some(0),
_ => None,
};
- let back = button(text(format!("‹ {}", t!("Back"))).size(12))
+ let back = button(text(t!("Back").into_owned()).size(12))
.on_press_maybe(back_step.map(|s| msg(SheetSetMsg::Wizard(WizardMsg::Step(s)))))
.style(button_style(false))
.padding([6, 14]);
let next: Element<'a, Message> = match next_step {
- Some(s) => button(text(format!("{} ›", t!("Next"))).size(12))
+ Some(s) => button(text(t!("Next").into_owned()).size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::Step(s))))
.style(button_style(true))
.padding([6, 14])
@@ -1578,8 +1589,16 @@ fn form_view<'a>(f: &'a Form) -> Element<'a, Message> {
let foot = match f.kind {
FormKind::Rename | FormKind::RenameView => footer(
vec![
- button(text(format!("‹ {}", t!("Previous"))).size(12)).on_press(msg(SheetSetMsg::Previous)).style(button_style(false)).padding([5, 12]).into(),
- button(text(format!("{} ›", t!("Next"))).size(12)).on_press(msg(SheetSetMsg::Next)).style(button_style(false)).padding([5, 12]).into(),
+ button(text(t!("Previous").into_owned()).size(12))
+ .on_press(msg(SheetSetMsg::Previous))
+ .style(button_style(false))
+ .padding([5, 12])
+ .into(),
+ button(text(t!("Next").into_owned()).size(12))
+ .on_press(msg(SheetSetMsg::Next))
+ .style(button_style(false))
+ .padding([5, 12])
+ .into(),
],
vec![
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into(),