diff --git a/crates/app/src/shot.rs b/crates/app/src/shot.rs index 8e4f29a..f0f0e98 100644 --- a/crates/app/src/shot.rs +++ b/crates/app/src/shot.rs @@ -90,6 +90,7 @@ enum Op { RegionData, XpsSetup, CraftSetup, + PanelControls(bool), XpsTab(plotx_core::state::XpsWorkbenchTab), /// Show a Ribbon task tab. Sets the state directly (like [`Op::XpsTab`]) /// so the capture shows the tab's command row without the side effects a @@ -197,6 +198,18 @@ const SCENES: &[Scene] = &[ shot(8, "xps_diagnostics"), act(2, Op::CraftSetup), shot(8, "craft_results"), + act(2, Op::PanelControls(false)), + shot(8, "panel_controls_collapsed"), + act(2, Op::Resize(720.0, 700.0)), + shot(8, "panel_controls_narrow"), + act(2, Op::Resize(1440.0, 900.0)), + act(2, Op::PanelControls(true)), + shot(8, "panel_controls_expanded"), + Scene { + settle: 4, + op: None, + shot: None, + }, ]; pub struct ShotDriver { @@ -379,6 +392,10 @@ fn run_op(op: Op, app: &mut PlotxApp, ctx: &egui::Context) -> Result<(), String> } Op::XpsSetup => xps_setup(app, ctx)?, Op::CraftSetup => craft_shot::setup(app, ctx)?, + Op::PanelControls(expanded) => { + app.session.ui.ribbon_expanded = expanded; + app.session.ui.craft_task_collapsed = !expanded; + } Op::XpsTab(tab) => app.session.ui.xps_workbench_tab = tab, Op::RibbonTab(tab) => app.session.ui.ribbon_tab = tab, Op::Zoom(factor) => ctx.set_zoom_factor(factor), @@ -773,24 +790,5 @@ fn save_png(path: &Path, image: &egui::ColorImage) -> Result<(), String> { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn automated_exit_bypasses_dirty_project_prompt() { - let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default()); - app.mark_document_dirty(); - let ctx = egui::Context::default(); - - let output = ctx.run_ui(egui::RawInput::default(), |ui| { - request_exit(&mut app, ui.ctx()); - }); - - assert!(app.session.allow_close); - let root = output - .viewport_output - .get(&egui::ViewportId::ROOT) - .expect("root viewport output"); - assert!(root.commands.contains(&egui::ViewportCommand::Close)); - } -} +#[path = "shot/tests.rs"] +mod tests; diff --git a/crates/app/src/shot/tests.rs b/crates/app/src/shot/tests.rs new file mode 100644 index 0000000..7cde2dd --- /dev/null +++ b/crates/app/src/shot/tests.rs @@ -0,0 +1,19 @@ +use super::*; + +#[test] +fn automated_exit_bypasses_dirty_project_prompt() { + let mut app = PlotxApp::new_with_settings(plotx_core::settings::Settings::default()); + app.mark_document_dirty(); + let ctx = egui::Context::default(); + + let output = ctx.run_ui(egui::RawInput::default(), |ui| { + request_exit(&mut app, ui.ctx()); + }); + + assert!(app.session.allow_close); + let root = output + .viewport_output + .get(&egui::ViewportId::ROOT) + .expect("root viewport output"); + assert!(root.commands.contains(&egui::ViewportCommand::Close)); +} diff --git a/crates/app/src/ui/mod.rs b/crates/app/src/ui/mod.rs index e47e927..4766cd8 100644 --- a/crates/app/src/ui/mod.rs +++ b/crates/app/src/ui/mod.rs @@ -22,6 +22,7 @@ mod menus; #[cfg(target_os = "macos")] pub(crate) mod native_menu; mod object_inspector; +mod panel_chrome; mod present; mod primary_sidebar; pub(crate) mod processing_templates; diff --git a/crates/app/src/ui/panel_chrome.rs b/crates/app/src/ui/panel_chrome.rs new file mode 100644 index 0000000..c26d2a5 --- /dev/null +++ b/crates/app/src/ui/panel_chrome.rs @@ -0,0 +1,75 @@ +use egui::{Color32, Rect, Response, Sense, Stroke, StrokeKind, Ui, vec2}; + +pub(super) const BUTTON_WIDTH: f32 = 30.0; + +#[derive(Clone, Copy)] +pub(super) enum Edge { + Left, + Right, + Top, + Bottom, +} + +pub(super) fn toggle(ui: &mut Ui, edge: Edge, visible: bool, label: &str) -> Response { + let (rect, response) = button(ui, label); + let color = if visible { + ui.style().interact(&response).text_color() + } else { + ui.visuals().weak_text_color() + }; + paint_glyph(ui, rect, edge, visible, color); + response.on_hover_text(label) +} + +pub(super) fn close(ui: &mut Ui, label: &str) -> Response { + ui.add_sized( + vec2(BUTTON_WIDTH, ui.spacing().interact_size.y), + egui::Button::new(egui_phosphor::regular::X).frame_when_inactive(false), + ) + .on_hover_text(label) +} + +fn button(ui: &mut Ui, label: &str) -> (Rect, Response) { + let (rect, response) = ui.allocate_exact_size( + vec2(BUTTON_WIDTH, ui.spacing().interact_size.y), + Sense::click(), + ); + response.widget_info(|| { + egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), label) + }); + let visuals = ui.style().interact(&response); + if response.hovered() || response.is_pointer_button_down_on() || response.has_focus() { + ui.painter() + .rect_filled(rect, visuals.corner_radius, visuals.weak_bg_fill); + } + (rect, response) +} + +fn paint_glyph(ui: &Ui, rect: Rect, edge: Edge, filled: bool, color: Color32) { + let painter = ui.painter(); + let outer = Rect::from_center_size(rect.center(), vec2(16.0, 12.0)); + painter.rect_stroke(outer, 3.0, Stroke::new(1.2_f32, color), StrokeKind::Inside); + let inner = outer.shrink(2.0); + let band = match edge { + Edge::Left => Rect::from_min_size(inner.min, vec2(5.0, inner.height())), + Edge::Right => Rect::from_min_size( + inner.right_top() - vec2(5.0, 0.0), + vec2(5.0, inner.height()), + ), + Edge::Top => Rect::from_min_size(inner.min, vec2(inner.width(), 3.0)), + Edge::Bottom => Rect::from_min_size( + inner.left_bottom() - vec2(0.0, 3.0), + vec2(inner.width(), 3.0), + ), + }; + if filled { + painter.rect_filled(band, 1.5, color); + } else { + painter.rect_stroke(band, 1.5, Stroke::new(1.0_f32, color), StrokeKind::Inside); + } +} + +pub(super) fn collapse(ui: &mut Ui, collapsed: bool, name: &str) -> Response { + let action = if collapsed { "Expand" } else { "Collapse" }; + toggle(ui, Edge::Bottom, !collapsed, &format!("{action} {name}")) +} diff --git a/crates/app/src/ui/ribbon.rs b/crates/app/src/ui/ribbon.rs index 9116ecd..6014835 100644 --- a/crates/app/src/ui/ribbon.rs +++ b/crates/app/src/ui/ribbon.rs @@ -177,22 +177,16 @@ fn render_chrome_controls( ui: &mut Ui, compact_controls: bool, ) { - let collapse_label = if compact_controls { + let collapse = super::panel_chrome::toggle( + ui, + super::panel_chrome::Edge::Top, + app.session.ui.ribbon_expanded, if app.session.ui.ribbon_expanded { - icon::CARET_UP.to_owned() + "Collapse ribbon" } else { - icon::CARET_DOWN.to_owned() - } - } else if app.session.ui.ribbon_expanded { - format!("{} Collapse ribbon", icon::CARET_UP) - } else { - format!("{} Expand ribbon", icon::CARET_DOWN) - }; - // The strip next to the task tabs stays quiet: chrome buttons show - // their frame only on hover so they read no heavier than the tabs. - let collapse = ui - .add(Button::new(collapse_label).frame_when_inactive(false)) - .on_hover_text("Collapse or expand the ribbon command area"); + "Expand ribbon" + }, + ); if collapse.clicked() { app.session.ui.ribbon_expanded = !app.session.ui.ribbon_expanded; } diff --git a/crates/app/src/ui/ribbon_chrome.rs b/crates/app/src/ui/ribbon_chrome.rs index 4539820..293aeb3 100644 --- a/crates/app/src/ui/ribbon_chrome.rs +++ b/crates/app/src/ui/ribbon_chrome.rs @@ -109,13 +109,6 @@ fn task_tabs_width(ui: &Ui) -> f32 { fn controls_width(app: &PlotxApp, ui: &Ui, compact: bool) -> f32 { use plotx_core::update::UpdateStatus; - let collapse = if compact { - icon::CARET_UP.to_owned() - } else if app.session.ui.ribbon_expanded { - format!("{} Collapse ribbon", icon::CARET_UP) - } else { - format!("{} Expand ribbon", icon::CARET_DOWN) - }; let search = if compact { icon::MAGNIFYING_GLASS.to_owned() } else { @@ -135,21 +128,21 @@ fn controls_width(app: &PlotxApp, ui: &Ui, compact: bool) -> f32 { _ => String::new(), }; let spacing = CONTROL_SPACING; - [collapse, search, update] + [search, update] .into_iter() .filter(|text| !text.is_empty()) .map(|text| text_width(ui, text, TextStyle::Button)) .sum::() - // The two sidebar layout toggles, the separator before them, and + // The two sidebar toggles, Ribbon toggle, separator, and // their share of the item spacing. - + 2.0 * SIDEBAR_TOGGLE_WIDTH + + 3.0 * SIDEBAR_TOGGLE_WIDTH + 6.0 + 5.0 * spacing } /// Fixed width of one sidebar layout toggle; shared with the width estimate /// in `controls_width` so compaction accounts for the pair. -pub(super) const SIDEBAR_TOGGLE_WIDTH: f32 = 30.0; +pub(super) const SIDEBAR_TOGGLE_WIDTH: f32 = super::panel_chrome::BUTTON_WIDTH; /// Fixed gap between task tabs. Command density applies only below this row, /// so switching tabs cannot move the task buttons or trailing chrome. @@ -171,69 +164,17 @@ pub(super) fn sidebar_toggle_button( use super::commands; let command = commands::describe(app, id); - let sidebar_visible = command.checked == Some(true); - let (rect, response) = ui.allocate_exact_size( - egui::vec2(SIDEBAR_TOGGLE_WIDTH, ui.spacing().interact_size.y), - egui::Sense::click(), - ); - if response.clicked() { - commands::execute(id, app, clipboard, ui.ctx()); - } - let visuals = ui.style().interact(&response); - if response.hovered() || response.is_pointer_button_down_on() { - // Match the neighbouring frameless chrome buttons: a quiet fill that - // appears only under the pointer. - ui.painter() - .rect_filled(rect, visuals.corner_radius, visuals.weak_bg_fill); - } - let color = if sidebar_visible { - visuals.text_color() - } else { - ui.visuals().weak_text_color() - }; - paint_sidebar_glyph( - ui, - rect, - id == commands::CommandId::TogglePrimarySidebar, - sidebar_visible, - color, - ); let tip = match &command.shortcut { Some(shortcut) => format!("{} ({shortcut})", command.label), None => command.label.clone(), }; - response.on_hover_text(tip); -} - -fn paint_sidebar_glyph(ui: &Ui, rect: egui::Rect, left: bool, filled: bool, color: egui::Color32) { - let painter = ui.painter(); - let outer = egui::Rect::from_center_size(rect.center(), egui::vec2(16.0, 12.0)); - painter.rect_stroke( - outer, - 3.0, - egui::Stroke::new(1.2_f32, color), - egui::StrokeKind::Inside, - ); - let band = if left { - egui::Rect::from_min_max( - outer.min + egui::vec2(2.0, 2.0), - egui::pos2(outer.min.x + 7.0, outer.max.y - 2.0), - ) + let edge = if id == commands::CommandId::TogglePrimarySidebar { + super::panel_chrome::Edge::Left } else { - egui::Rect::from_min_max( - egui::pos2(outer.max.x - 7.0, outer.min.y + 2.0), - outer.max - egui::vec2(2.0, 2.0), - ) + super::panel_chrome::Edge::Right }; - if filled { - painter.rect_filled(band, 1.5, color); - } else { - painter.rect_stroke( - band, - 1.5, - egui::Stroke::new(1.0_f32, color), - egui::StrokeKind::Inside, - ); + if super::panel_chrome::toggle(ui, edge, command.checked == Some(true), &tip).clicked() { + commands::execute(id, app, clipboard, ui.ctx()); } } diff --git a/crates/app/src/ui/tools/craft.rs b/crates/app/src/ui/tools/craft.rs index 0f61ac9..90a6a95 100644 --- a/crates/app/src/ui/tools/craft.rs +++ b/crates/app/src/ui/tools/craft.rs @@ -1,5 +1,4 @@ use egui::{Button, Ui}; -use egui_phosphor::regular as icon; use plotx_core::state::{ CraftAnalysisIntent, CraftTaskPage, Dataset, FrameRef, PlotxApp, Selection, TaskDockTab, Tool, }; @@ -207,27 +206,10 @@ pub(crate) fn render_task(app: &mut PlotxApp, host: &mut Ui) { ui.set_width(width); crate::ui::card_frame(dark, egui::Margin::ZERO).show(ui, |ui| { task_card::header(ui, area_id, "CRAFT", None::<&str>, |ui| { - if ui - .small_button(icon::X) - .on_hover_text("Close CRAFT") - .clicked() - { + if crate::ui::panel_chrome::close(ui, "Close CRAFT").clicked() { close = true; } - let glyph = if collapsed { - icon::CARET_DOWN - } else { - icon::CARET_UP - }; - if ui - .small_button(glyph) - .on_hover_text(if collapsed { - "Expand CRAFT" - } else { - "Collapse CRAFT" - }) - .clicked() - { + if crate::ui::panel_chrome::collapse(ui, collapsed, "CRAFT").clicked() { toggle_collapse = true; } }); diff --git a/crates/app/src/ui/tools/curve_fit.rs b/crates/app/src/ui/tools/curve_fit.rs index 5ca247d..8200961 100644 --- a/crates/app/src/ui/tools/curve_fit.rs +++ b/crates/app/src/ui/tools/curve_fit.rs @@ -82,27 +82,10 @@ pub(crate) fn render_task(app: &mut PlotxApp, host: &mut Ui) { "Curve Fit", Some(format!("{curve_count} · {points} points each")), |ui| { - if ui - .small_button(icon::X) - .on_hover_text("Close Curve Fit") - .clicked() - { + if crate::ui::panel_chrome::close(ui, "Close Curve Fit").clicked() { close = true; } - let glyph = if collapsed { - icon::CARET_DOWN - } else { - icon::CARET_UP - }; - if ui - .small_button(glyph) - .on_hover_text(if collapsed { - "Expand Curve Fit" - } else { - "Collapse Curve Fit" - }) - .clicked() - { + if crate::ui::panel_chrome::collapse(ui, collapsed, "Curve Fit").clicked() { toggle_collapse = true; } }, diff --git a/crates/app/src/ui/tools/processing/surface.rs b/crates/app/src/ui/tools/processing/surface.rs index bf43cd5..ac5b1d0 100644 --- a/crates/app/src/ui/tools/processing/surface.rs +++ b/crates/app/src/ui/tools/processing/surface.rs @@ -140,19 +140,10 @@ pub(super) fn render(app: &mut PlotxApp, host: &mut Ui) { "Frequency-domain output" }; task_card::header(ui, area_id, "Processing", Some(domain), |ui| { - if ui - .small_button(icon::X) - .on_hover_text("Close Processing") - .clicked() - { + if crate::ui::panel_chrome::close(ui, "Close Processing").clicked() { close = true; } - let glyph = if collapsed { - icon::CARET_DOWN - } else { - icon::CARET_UP - }; - if ui.small_button(glyph).clicked() { + if crate::ui::panel_chrome::collapse(ui, collapsed, "Processing").clicked() { toggle = true; } ui.menu_button(icon::DOTS_THREE_VERTICAL, |ui| panel_menu(app, di, ui)); @@ -238,19 +229,10 @@ fn render_xrd(app: &mut PlotxApp, host: &mut Ui, di: usize) { ui.set_width(width); crate::ui::card_frame(dark, egui::Margin::ZERO).show(ui, |ui| { task_card::header(ui, area_id, "XRD Processing", None::<&str>, |ui| { - if ui - .small_button(icon::X) - .on_hover_text("Close Processing") - .clicked() - { + if crate::ui::panel_chrome::close(ui, "Close Processing").clicked() { close = true; } - let glyph = if collapsed { - icon::CARET_DOWN - } else { - icon::CARET_UP - }; - if ui.small_button(glyph).clicked() { + if crate::ui::panel_chrome::collapse(ui, collapsed, "Processing").clicked() { toggle = true; } ui.menu_button(icon::DOTS_THREE_VERTICAL, |ui| panel_menu(app, di, ui)); diff --git a/crates/app/src/ui/tools/region_analysis.rs b/crates/app/src/ui/tools/region_analysis.rs index 2d6fe2f..d64e58c 100644 --- a/crates/app/src/ui/tools/region_analysis.rs +++ b/crates/app/src/ui/tools/region_analysis.rs @@ -93,27 +93,10 @@ pub(crate) fn render_task(app: &mut PlotxApp, host: &mut Ui) { format!("{count} regions") }; task_card::header(ui, area_id, "Regions", Some(state), |ui| { - if ui - .small_button(icon::X) - .on_hover_text("Close region tools") - .clicked() - { + if crate::ui::panel_chrome::close(ui, "Close region tools").clicked() { close = true; } - let glyph = if collapsed { - icon::CARET_DOWN - } else { - icon::CARET_UP - }; - if ui - .small_button(glyph) - .on_hover_text(if collapsed { - "Expand region tools" - } else { - "Collapse region tools" - }) - .clicked() - { + if crate::ui::panel_chrome::collapse(ui, collapsed, "region tools").clicked() { toggle_collapse = true; } if collapsed diff --git a/crates/app/src/ui/tools/statistics.rs b/crates/app/src/ui/tools/statistics.rs index 02c7dfc..7d9525e 100644 --- a/crates/app/src/ui/tools/statistics.rs +++ b/crates/app/src/ui/tools/statistics.rs @@ -83,27 +83,10 @@ pub(crate) fn render_task(app: &mut PlotxApp, host: &mut Ui) { "Statistics", Some(format!("{columns} columns · {points} rows")), |ui| { - if ui - .small_button(icon::X) - .on_hover_text("Close Statistics") - .clicked() - { + if crate::ui::panel_chrome::close(ui, "Close Statistics").clicked() { close = true; } - let glyph = if collapsed { - icon::CARET_DOWN - } else { - icon::CARET_UP - }; - if ui - .small_button(glyph) - .on_hover_text(if collapsed { - "Expand Statistics" - } else { - "Collapse Statistics" - }) - .clicked() - { + if crate::ui::panel_chrome::collapse(ui, collapsed, "Statistics").clicked() { toggle_collapse = true; } }, diff --git a/docs/src/content/docs/reference/ribbon.md b/docs/src/content/docs/reference/ribbon.md index 26f90f5..5ae2e68 100644 --- a/docs/src/content/docs/reference/ribbon.md +++ b/docs/src/content/docs/reference/ribbon.md @@ -49,7 +49,7 @@ The Ribbon measures the active tab's content against the window: lower priority, and every group keeps its position. - Only when every group is already a single button and the row still does not fit do whole groups move into the **More** menu. -- The command area never hides by itself. **Collapse ribbon** (the caret at +- The command area never hides by itself. **Collapse ribbon** (the window icon with a top band at the right end of the task row) puts it away and brings it back. Buttons that start a computation, such as **Run Peak Fit**, use the filled diff --git a/docs/src/content/docs/reference/ui-overview.md b/docs/src/content/docs/reference/ui-overview.md index 8418b18..3b7b593 100644 --- a/docs/src/content/docs/reference/ui-overview.md +++ b/docs/src/content/docs/reference/ui-overview.md @@ -45,6 +45,10 @@ buttons at the right end of the Ribbon's task row, press (right; Cmd on macOS), or drag a Side Bar's inner edge past its minimum width to hide it. +The Ribbon's show/hide button uses the same window-outline style, with a band +at the top. Task cards use a bottom band for their body; a filled band means +the region is visible. Closing a task card remains a separate **×** button. + ## Recurring elements - **Figure panel (Panel)** — a labelled section of a figure that can contain diff --git a/docs/src/content/docs/zh-cn/reference/ribbon.md b/docs/src/content/docs/zh-cn/reference/ribbon.md index d04b641..9b56c47 100644 --- a/docs/src/content/docs/zh-cn/reference/ribbon.md +++ b/docs/src/content/docs/zh-cn/reference/ribbon.md @@ -42,7 +42,7 @@ Ribbon 以活动页签的内容对照窗口宽度实测: 一个带组名的按钮,点击即在弹出面板中展开整组。任何分组都不会比优先级更低的 分组缩得更小,且每个分组始终留在原位。 - 只有当所有分组都已折成单个按钮而仍然放不下时,整组才会移入 **More** 菜单。 -- 命令区不会自行隐藏。**Collapse ribbon**(任务行右端的箭头)负责收起与 +- 命令区不会自行隐藏。**Collapse ribbon**(任务行右端带顶部条带的窗口图标)负责收起与 展开。 启动计算的按钮(如 **Run Peak Fit**)使用强调色填充样式——与任务卡片的 diff --git a/docs/src/content/docs/zh-cn/reference/ui-overview.md b/docs/src/content/docs/zh-cn/reference/ui-overview.md index 89ee82a..66509b2 100644 --- a/docs/src/content/docs/zh-cn/reference/ui-overview.md +++ b/docs/src/content/docs/zh-cn/reference/ui-overview.md @@ -38,6 +38,10 @@ PlotX 的界面为英文;手册中加粗的英文词即界面上的原文标 Ctrl+B(左)或 Ctrl+Shift+B (右;macOS 上为 Cmd),或把侧栏内缘拖过其最小宽度即可将其隐藏。 +Ribbon 的显示/隐藏按钮沿用相同的窗口轮廓样式,顶部条带表示命令区。 +任务卡片使用底部条带表示内容区;实心条带表示该区域可见。 +关闭任务卡片仍使用独立的 **×** 按钮。 + ## 常见元素 - **图版面板(Panel)**——图版中带标签的分区,可以包含图表、图片、文本与形状。