Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions crates/app/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ mod workspace_geometry;

use data_sheet::*;
use diagnostics::*;
use egui::{Color32, Id, InnerResponse, Mesh, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2};
use egui::{
Color32, Id, InnerResponse, Mesh, Pos2, Rect, Response, Sense, Stroke, Ui, UiBuilder, Vec2,
};
use export_dialog::*;
use plotx_core::actions::Action;
use plotx_core::export::{ExportPageScope, ExportScopeKind, ExportSettings};
Expand Down Expand Up @@ -162,12 +164,21 @@ pub fn render(
),
))
.show_inside(ui, |ui| {
canvas::render_central(app, ui);
tools::render_processing_task(app, ui);
tools::render_region_task(app, ui);
tools::render_curve_fit_task(app, ui);
tools::render_statistics_task(app, ui);
tools::render_craft_task(app, ui);
// Same stable id scope as the sidebars: the central panel's ids
// must not shift when a sidebar toggles (see `show_sidebar`).
ui.scope_builder(
UiBuilder::new()
.id_salt(Id::new("central_workspace_stable_scope"))
.global_scope(true),
|ui| {
canvas::render_central(app, ui);
tools::render_processing_task(app, ui);
tools::render_region_task(app, ui);
tools::render_curve_fit_task(app, ui);
tools::render_statistics_task(app, ui);
tools::render_craft_task(app, ui);
},
);
});

canvas_settings_window(app, &ctx);
Expand Down
132 changes: 83 additions & 49 deletions crates/app/src/ui/ribbon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,57 +162,91 @@ fn render_task_row_contents(
}

ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let auto_collapsed = density == RibbonDensity::Collapsed && app.session.ui.ribbon_expanded;
let full_collapse_label = if auto_collapsed {
format!("{} Ribbon auto-collapsed", icon::CARET_DOWN)
} else if app.session.ui.ribbon_expanded {
format!("{} Collapse ribbon", icon::CARET_UP)
} else {
format!("{} Expand ribbon", icon::CARET_DOWN)
};
let collapse_label = if compact_controls {
if app.session.ui.ribbon_expanded {
icon::CARET_UP.to_owned()
} else {
icon::CARET_DOWN.to_owned()
}
} else {
full_collapse_label
};
// 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_enabled(
!auto_collapsed,
Button::new(collapse_label).frame_when_inactive(false),
);
let collapse = if auto_collapsed {
collapse.on_disabled_hover_text(
"The ribbon collapses automatically at this width; use menus or Search commands",
)
} else {
collapse.on_hover_text("Collapse or expand the ribbon command area")
};
if collapse.clicked() {
app.session.ui.ribbon_expanded = !app.session.ui.ribbon_expanded;
}
update_button(app, ui, compact_controls);
let palette = commands::describe(app, CommandId::CommandPalette);
let search_label = if compact_controls {
icon::MAGNIFYING_GLASS.to_owned()
// Stable id scope: siblings earlier in this row (the inline project
// title, tab labels) come and go with app state; without this every
// control in the strip changes id when they do, which drops focus
// and trips egui's rect-changed-id debug overlay.
let scope = UiBuilder::new()
.id_salt(egui::Id::new("ribbon_chrome_controls"))
.global_scope(true);
ui.scope_builder(scope, |ui| {
render_chrome_controls(app, clipboard, ui, density, compact_controls)
});
});
}

fn render_chrome_controls(
app: &mut PlotxApp,
clipboard: &mut ClipboardTablePaste,
ui: &mut Ui,
density: RibbonDensity,
compact_controls: bool,
) {
let auto_collapsed = density == RibbonDensity::Collapsed && app.session.ui.ribbon_expanded;
let full_collapse_label = if auto_collapsed {
format!("{} Ribbon auto-collapsed", icon::CARET_DOWN)
} else if app.session.ui.ribbon_expanded {
format!("{} Collapse ribbon", icon::CARET_UP)
} else {
format!("{} Expand ribbon", icon::CARET_DOWN)
};
let collapse_label = if compact_controls {
if app.session.ui.ribbon_expanded {
icon::CARET_UP.to_owned()
} else {
format!("{} Search commands", icon::MAGNIFYING_GLASS)
};
if ui
.add(Button::new(search_label).frame_when_inactive(false))
.on_hover_text(format!(
"Search every command ({})",
palette.shortcut.as_deref().unwrap_or("Ctrl+K")
))
.clicked()
{
commands::execute(CommandId::CommandPalette, app, clipboard, ui.ctx());
icon::CARET_DOWN.to_owned()
}
});
} else {
full_collapse_label
};
// 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_enabled(
!auto_collapsed,
Button::new(collapse_label).frame_when_inactive(false),
);
let collapse = if auto_collapsed {
collapse.on_disabled_hover_text(
"The ribbon collapses automatically at this width; use menus or Search commands",
)
} else {
collapse.on_hover_text("Collapse or expand the ribbon command area")
};
if collapse.clicked() {
app.session.ui.ribbon_expanded = !app.session.ui.ribbon_expanded;
}
update_button(app, ui, compact_controls);
let palette = commands::describe(app, CommandId::CommandPalette);
let search_label = if compact_controls {
icon::MAGNIFYING_GLASS.to_owned()
} else {
format!("{} Search commands", icon::MAGNIFYING_GLASS)
};
if ui
.add(Button::new(search_label).frame_when_inactive(false))
.on_hover_text(format!(
"Search every command ({})",
palette.shortcut.as_deref().unwrap_or("Ctrl+K")
))
.clicked()
{
commands::execute(CommandId::CommandPalette, app, clipboard, ui.ctx());
}
ui.separator();
// Right-to-left layout: added secondary-first so the pair reads
// [left sidebar][right sidebar], mirroring the window.
super::ribbon_chrome::sidebar_toggle_button(
app,
clipboard,
ui,
CommandId::ToggleSecondarySidebar,
);
super::ribbon_chrome::sidebar_toggle_button(
app,
clipboard,
ui,
CommandId::TogglePrimarySidebar,
);
}

fn select_workflow_tab(app: &mut PlotxApp, tab: WorkflowTab) {
Expand Down
89 changes: 88 additions & 1 deletion crates/app/src/ui/ribbon_chrome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,94 @@ fn controls_width(app: &PlotxApp, ui: &Ui, density: RibbonDensity, compact: bool
.filter(|text| !text.is_empty())
.map(|text| text_width(ui, text, TextStyle::Button))
.sum::<f32>()
+ 2.0 * spacing
// The two sidebar layout toggles, the separator before them, and
// their share of the item spacing.
+ 2.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;

/// Always-visible sidebar toggle for the task row. The glyph mirrors the
/// window layout: the band marks which side the command controls and is
/// filled while that sidebar is visible, so the pair doubles as a live
/// layout indicator.
pub(super) fn sidebar_toggle_button(
app: &mut PlotxApp,
clipboard: &mut super::clipboard_table::ClipboardTablePaste,
ui: &mut Ui,
id: super::commands::CommandId,
) {
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),
)
} 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),
)
};
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,
);
}
}

fn text_width(ui: &Ui, text: impl Into<WidgetText>, fallback: TextStyle) -> f32 {
Expand Down
7 changes: 7 additions & 0 deletions crates/app/src/ui/shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ static BINDINGS: &[CommandBinding] = &[
bound(commands::CommandId::Group, cmd(egui::Key::G)),
bound(commands::CommandId::Ungroup, cmd_shift(egui::Key::G)),
bound(commands::CommandId::Preferences, cmd(egui::Key::Comma)),
// The VS Code sidebar chords. The layout buttons in the Ribbon task row
// are the discoverable surface; these are the fast path.
bound(commands::CommandId::TogglePrimarySidebar, cmd(egui::Key::B)),
bound(
commands::CommandId::ToggleSecondarySidebar,
cmd_shift(egui::Key::B),
),
CommandBinding {
id: commands::CommandId::UiScaleUp,
primary: cmd(egui::Key::Plus),
Expand Down
Loading
Loading