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(),