Title bar: inactive tabs without outlines, File menu, Help on the right (#1277)
* Title bar: inactive tabs without outlines, File menu, Help on the right Inactive tabs go back to the plain bar colour (the Home tab's readable hover stays). The separate Home button and theme toggle are gone: Home is a tab and the theme is in Preferences. The folder icon becomes a File text menu, Save moves into it (Ctrl+S unchanged), and Help sits on the right next to Report a bug. Tabs start right after File, Undo and Redo. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Title bar: File button hover uses the app bar hover token Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bd8db8ea1c
commit
406293fd53
7 files changed
+117
-71
No files matched your search
@@ -76,7 +76,6 @@ impl Tokens {
|
||||
rubber_band: Color32::from_rgb(30, 120, 230),
|
||||
app_bar_text: Color32::from_rgb(230, 234, 242),
|
||||
app_bar_hover: Color32::from_rgba_unmultiplied_const(255, 255, 255, 28),
|
||||
app_bar_tab_edge: Color32::from_rgba_unmultiplied_const(255, 255, 255, 40),
|
||||
timeline_item: Color32::WHITE,
|
||||
timeline_item_error: Color32::from_rgb(250, 220, 220),
|
||||
timeline_marker: Color32::from_rgb(70, 76, 88),
|
||||
|
||||
@@ -373,7 +373,6 @@ const NO_MEANING: &[(&str, &str)] = &[
|
||||
("overlay", "a chip's ground over the view"),
|
||||
("app_bar_text", "text"),
|
||||
("app_bar_hover", "a hovered tab, translucent white"),
|
||||
("app_bar_tab_edge", "an inactive tab's outline, translucent white"),
|
||||
("timeline_marker", "the rollback marker, alone"),
|
||||
("field", "a field's ground"),
|
||||
// Highlights: shades of the accent blue over what they mark, by design close to each other.
|
||||
|
||||
@@ -455,7 +455,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) {
|
||||
}
|
||||
|
||||
/// One tab of the strip: the active tab takes the toolbar's colour and runs into it; the others
|
||||
/// sit on the application bar with a faint outline (no bottom edge) so neighbours stay apart.
|
||||
/// sit on the application bar, lighter while hovered.
|
||||
fn tab_shape(painter: &egui::Painter, t: &Tokens, r: Rect, active: bool, hovered: bool) {
|
||||
let corners = egui::CornerRadius { nw: 5, ne: 5, sw: 0, se: 0 };
|
||||
let fill = if active {
|
||||
@@ -466,11 +466,6 @@ fn tab_shape(painter: &egui::Painter, t: &Tokens, r: Rect, active: bool, hovered
|
||||
t.app_bar
|
||||
};
|
||||
painter.rect_filled(r, corners, fill);
|
||||
if !active {
|
||||
// Drawn past the bottom and clipped, so only the top and the sides show.
|
||||
let outline = Rect::from_min_max(r.min, pos2(r.max.x, r.max.y + 2.0));
|
||||
painter.with_clip_rect(r).rect_stroke(outline, corners, Stroke::new(1.0, t.app_bar_tab_edge), egui::StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which tabs the strip shows in `room` points: (first, how many, tab width). Tabs shrink to
|
||||
@@ -629,10 +624,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The tabs sit on the dark application bar in both themes: a hovered tab (the Home tab
|
||||
/// included) keeps the bar's light text readable, and an inactive tab is outlined so
|
||||
/// neighbouring tabs stay apart.
|
||||
/// included) keeps the bar's light text readable, and no tab is outlined.
|
||||
#[test]
|
||||
fn title_bar_tabs_are_outlined_and_keep_their_text_readable_on_hover() {
|
||||
fn title_bar_tabs_keep_their_text_readable_on_hover() {
|
||||
fn luminance(c: Color32) -> f32 {
|
||||
let c = egui::Rgba::from(c);
|
||||
0.2126 * c.r() + 0.7152 * c.g() + 0.0722 * c.b()
|
||||
@@ -662,11 +656,7 @@ mod tests {
|
||||
let (hi, lo) = (luminance(t.app_bar_text).max(luminance(bg)), luminance(t.app_bar_text).min(luminance(bg)));
|
||||
assert!((hi + 0.05) / (lo + 0.05) >= 4.5, "text on a tab (hovered: {hovered}) must stay readable: {bg:?} {t:?}");
|
||||
}
|
||||
let outline = rects.iter().find(|(rect, _)| rect.stroke.width > 0.0);
|
||||
assert_eq!(outline.is_some(), !active, "only inactive tabs are outlined (active: {active})");
|
||||
if let Some((rect, clip)) = outline {
|
||||
assert!(rect.rect.bottom() > r.bottom() && clip.bottom() <= r.bottom(), "the outline has no bottom edge");
|
||||
}
|
||||
assert!(rects.iter().all(|(rect, _)| rect.stroke.width == 0.0), "tabs have no outline (active: {active})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +266,6 @@ pub struct Tokens {
|
||||
pub app_bar_text: Color32,
|
||||
/// A hovered tab on the application bar (keeps `app_bar_text` readable).
|
||||
pub app_bar_hover: Color32,
|
||||
/// Outline of the inactive tabs on the application bar, so neighbouring tabs stay apart.
|
||||
pub app_bar_tab_edge: Color32,
|
||||
/// Timeline items: normal and failed.
|
||||
pub timeline_item: Color32,
|
||||
pub timeline_item_error: Color32,
|
||||
|
||||
@@ -286,7 +286,7 @@ mod tests {
|
||||
fn empty_bar_space_drags_and_double_click_maximizes() {
|
||||
let mut w = Win::new(1600.0);
|
||||
w.frame(vec![]);
|
||||
let empty = pos2(500.0, 17.0);
|
||||
let empty = pos2(900.0, 17.0);
|
||||
let cmds = w.gesture(&[empty, empty + vec2(12.0, 4.0), empty + vec2(30.0, 8.0)]);
|
||||
assert_eq!(cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(), 1, "{cmds:?}");
|
||||
let mut cmds = w.click(empty);
|
||||
@@ -299,7 +299,7 @@ mod tests {
|
||||
assert!(cmds.contains(&ViewportCommand::Maximized(false)), "{cmds:?}");
|
||||
// The bar's buttons keep their clicks: dragging from Undo doesn't move the window.
|
||||
w.time += 1.0;
|
||||
let undo = pos2(128.0 + 2.0 * 30.0, 17.0);
|
||||
let undo = crate::scenario::handle_at("appbar:undo").unwrap();
|
||||
let cmds = w.gesture(&[undo, undo + vec2(12.0, 4.0), undo + vec2(30.0, 8.0)]);
|
||||
assert!(!cmds.iter().any(|c| matches!(c, ViewportCommand::StartDrag)), "{cmds:?}");
|
||||
}
|
||||
|
||||
+106
-48
@@ -146,10 +146,19 @@ fn menu_commands(
|
||||
out
|
||||
}
|
||||
|
||||
/// What the right-hand group keeps clear between Report a bug and the window's caption buttons:
|
||||
/// Help, Preferences and the version.
|
||||
const RIGHT_GROUP: f32 = 120.0;
|
||||
|
||||
/// The centre of the Help (?) button: left of Preferences.
|
||||
fn help_x(bar: Rect, captions: f32) -> f32 {
|
||||
bar.right() - captions - 101.0
|
||||
}
|
||||
|
||||
/// The Report a bug button: labelled when there is room for its text (`label_width`, measured in
|
||||
/// the current language), otherwise just the icon.
|
||||
fn report_rect(bar: Rect, captions: f32, tabs_start: f32, label_width: f32) -> (Rect, bool) {
|
||||
let right = bar.right() - captions - 90.0;
|
||||
let right = bar.right() - captions - RIGHT_GROUP;
|
||||
// Icon, text and a margin; never narrower than the English button.
|
||||
let full = (REPORT_TEXT_X + label_width + 10.0).max(124.0);
|
||||
let labeled = right - tabs_start >= 216.0 + full;
|
||||
@@ -184,12 +193,34 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui.painter().text(pos2(left + 36.0, r.center().y), Align2::LEFT_CENTER, "SolidCraft", FontId::proportional(14.0), t.app_bar_text);
|
||||
}
|
||||
let mut x = left + if compact_actions { 60.0 } else { 128.0 };
|
||||
// While a file loads the design waits: no menus or commands from the bar.
|
||||
let idle = !app.file_load.busy();
|
||||
// The File menu: a text button (New, Open, Save, Save As, Export…). Its handle keeps the
|
||||
// old `appbar:folder` name too, for scripts written against the folder button.
|
||||
let file = {
|
||||
let label = app.tr("ui.file").to_string();
|
||||
let galley = ui.painter().layout_no_wrap(label.clone(), FontId::proportional(13.0), t.app_bar_text);
|
||||
let br = Rect::from_min_size(pos2(x - 13.0, r.center().y - 13.0), vec2(galley.size().x + 16.0, 26.0));
|
||||
x = br.right() + 17.0;
|
||||
let resp = ui.interact(br, ui.id().with(("ab", "file")), if idle { Sense::click() } else { Sense::hover() });
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, idle, &label));
|
||||
crate::scenario::publish_handle("appbar:file", br.center());
|
||||
crate::scenario::publish_handle("appbar:folder", br.center());
|
||||
let open = ui.ctx().data(|d| d.get_temp::<bool>(egui::Id::new("sc_file_menu")).unwrap_or(false));
|
||||
if idle && (resp.hovered() || open) {
|
||||
ui.painter().rect_filled(br, 4.0, t.app_bar_hover);
|
||||
}
|
||||
let ink = if idle { t.app_bar_text } else { t.app_bar_text.gamma_multiply(0.35) };
|
||||
ui.painter().galley(pos2(br.left() + 8.0, br.center().y - galley.size().y / 2.0), galley, ink);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("sc_file_menu_x"), br.left()));
|
||||
idle && resp.clicked()
|
||||
};
|
||||
// A disabled button is dimmed and ignores clicks (its tooltip still shows).
|
||||
let mut click = |ui: &mut egui::Ui, icon: &str, tip: &str, enabled: bool| -> bool {
|
||||
let click = |ui: &mut egui::Ui, at: f32, icon: &str, tip: &str, enabled: bool| -> bool {
|
||||
let width = if compact_actions { 22.0 } else { 26.0 };
|
||||
let br = Rect::from_center_size(pos2(x, r.center().y), vec2(width, 26.0));
|
||||
x += if compact_actions { 22.0 } else { 30.0 };
|
||||
let br = Rect::from_center_size(pos2(at, r.center().y), vec2(width, 26.0));
|
||||
let resp = ui.interact(br, ui.id().with(("ab", icon)), if enabled { Sense::click() } else { Sense::hover() });
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, tip));
|
||||
crate::scenario::publish_handle(&format!("appbar:{icon}"), br.center());
|
||||
if enabled && resp.hovered() {
|
||||
ui.painter().rect_filled(br, 4.0, Color32::from_white_alpha(30));
|
||||
@@ -199,61 +230,53 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let clicked = resp.on_hover_text(tip).clicked();
|
||||
enabled && clicked
|
||||
};
|
||||
// While a file loads the design waits: no menus or commands from the bar.
|
||||
let idle = !app.file_load.busy();
|
||||
let file = click(ui, "folder", app.tr("ui.file"), idle);
|
||||
// Save, Undo and Redo act on the design, which Home hides.
|
||||
let step = if compact_actions { 22.0 } else { 30.0 };
|
||||
// Undo and Redo act on the design, which Home hides. Save lives in the File menu (Ctrl+S).
|
||||
let shown = design_shown(app) && idle;
|
||||
let tip = |id: &str, key: &str| {
|
||||
let label = app.tr(id);
|
||||
if shown { format!("{label} ({key})") } else { crate::i18n::format("ui.needs_design", &[("command", label)]) }
|
||||
};
|
||||
let (save_tip, undo_tip, redo_tip) = (tip("ui.save", "Ctrl+S"), tip("ui.undo", "Ctrl+Z"), tip("ui.redo", "Ctrl+Y"));
|
||||
if click(ui, "save", &save_tip, shown) {
|
||||
save(app);
|
||||
}
|
||||
if click(ui, "undo", &undo_tip, shown) {
|
||||
let _ = app.run("edit.undo", json!({}));
|
||||
}
|
||||
if click(ui, "redo", &redo_tip, shown) {
|
||||
let _ = app.run("edit.redo", json!({}));
|
||||
}
|
||||
if click(ui, "home", app.tr("ui.home"), idle) {
|
||||
let rev = app.session.revision;
|
||||
app.home.show(rev);
|
||||
}
|
||||
let theme_tip = if app.ui.dark { app.tr("ui.light") } else { app.tr("ui.dark") };
|
||||
if !compact_actions && click(ui, if app.ui.dark { "sun" } else { "moon" }, theme_tip, true) {
|
||||
app.ui.dark = !app.ui.dark;
|
||||
app.preferences.theme = if app.ui.dark { "dark" } else { "light" }.into();
|
||||
}
|
||||
if click(ui, "help", app.tr("ui.help"), true) {
|
||||
app.help.menu = !app.help.menu;
|
||||
let at = ui.ctx().input(|i| i.pointer.latest_pos()).map_or(300.0, |p| p.x);
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(egui::Id::new("sc_help_opened"), true);
|
||||
d.insert_temp(egui::Id::new("sc_help_anchor"), at);
|
||||
});
|
||||
let (undo_tip, redo_tip) = (tip("ui.undo", "Ctrl+Z"), tip("ui.redo", "Ctrl+Y"));
|
||||
// A very narrow window keeps its room for the tabs (Home first); Ctrl+Z and Ctrl+Y still undo and redo.
|
||||
if !compact_actions {
|
||||
if click(ui, x, "undo", &undo_tip, shown) {
|
||||
let _ = app.run("edit.undo", json!({}));
|
||||
}
|
||||
x += step;
|
||||
if click(ui, x, "redo", &redo_tip, shown) {
|
||||
let _ = app.run("edit.redo", json!({}));
|
||||
}
|
||||
x += step;
|
||||
}
|
||||
if file {
|
||||
app.ui.palette_open = false;
|
||||
let ctx = ui.ctx().clone();
|
||||
let open = ctx.data(|d| d.get_temp::<bool>(egui::Id::new("sc_file_menu")).unwrap_or(false));
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
// The folder button toggles the menu. Remember the pass it opened on so the same
|
||||
// The File button toggles the menu. Remember the pass it opened on so the same
|
||||
// click isn't also treated as a click outside the menu (which would close it at once).
|
||||
ctx.data_mut(|d| {
|
||||
d.insert_temp(egui::Id::new("sc_file_menu"), !open);
|
||||
d.insert_temp(egui::Id::new("sc_file_menu_opened"), pass);
|
||||
});
|
||||
}
|
||||
// Document tabs.
|
||||
// Document tabs (Home first), on the left after File, Undo and Redo.
|
||||
let captions = if app.custom_titlebar { crate::titlebar::WIDTH } else { 0.0 };
|
||||
let tabs_start = x + 16.0;
|
||||
let tabs_start = x - step + 25.0;
|
||||
let report_text = crate::i18n::tr("ui.report_a_bug");
|
||||
let report_font = FontId::proportional(13.0);
|
||||
let label_width = ui.painter().layout_no_wrap(report_text.to_string(), report_font.clone(), t.app_bar_text).size().x;
|
||||
let (report, report_label) = report_rect(r, captions, tabs_start, label_width);
|
||||
// Help, on the right between Report a bug and Preferences.
|
||||
if click(ui, help_x(r, captions), "help", app.tr("ui.help"), true) {
|
||||
app.help.menu = !app.help.menu;
|
||||
let at = help_x(r, captions);
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(egui::Id::new("sc_help_opened"), true);
|
||||
d.insert_temp(egui::Id::new("sc_help_anchor"), at);
|
||||
});
|
||||
}
|
||||
// A newer release: its Update button, left of Report a bug (`update`).
|
||||
let update = crate::update::button(app, ui, report.left() - 6.0, report.top(), report_label);
|
||||
let tabs_end = update.map_or(report.left(), |u| u.left()) - 8.0;
|
||||
@@ -315,7 +338,7 @@ fn file_menu(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
let mut close = false;
|
||||
let resp = crate::frame::area(egui::Id::new("sc_file_area"))
|
||||
.fixed_pos(pos2(100.0 + if app.integrated_titlebar { crate::titlebar::TRAFFIC_LIGHTS } else { 0.0 }, 34.0))
|
||||
.fixed_pos(pos2(ctx.data(|d| d.get_temp::<f32>(egui::Id::new("sc_file_menu_x"))).unwrap_or(100.0), 34.0))
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
@@ -1288,10 +1311,11 @@ mod header_report_tests {
|
||||
let (report, labeled) = report_rect(bar, captions, 354.0, label_width);
|
||||
assert!(bar.contains_rect(report));
|
||||
let prefs = Rect::from_center_size(pos2(width - captions - 70.0, 17.0), vec2(24.0, 24.0));
|
||||
assert!(!report.intersects(prefs));
|
||||
assert!(report.right() < width - captions - 82.0);
|
||||
let help = Rect::from_center_size(pos2(help_x(bar, captions), 17.0), vec2(26.0, 26.0));
|
||||
assert!(!report.intersects(prefs) && !report.intersects(help) && !help.intersects(prefs));
|
||||
assert!(report.right() < help.left() && help.right() < prefs.left());
|
||||
let full = (REPORT_TEXT_X + label_width + 10.0).max(124.0);
|
||||
assert_eq!(labeled, width - captions - 90.0 - 354.0 >= 216.0 + full);
|
||||
assert_eq!(labeled, width - captions - RIGHT_GROUP - 354.0 >= 216.0 + full);
|
||||
if labeled {
|
||||
assert!(REPORT_TEXT_X + label_width < report.width(), "{label_width}: the text overruns its button");
|
||||
}
|
||||
@@ -1301,6 +1325,39 @@ mod header_report_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The title bar: the File menu, Undo, Redo and every tab on the left; Help, Report a bug,
|
||||
/// Preferences and the window's buttons on the right. Home is a tab and the theme a
|
||||
/// preference, so neither has a button of its own, and Save is in the File menu (Ctrl+S).
|
||||
#[test]
|
||||
fn title_bar_keeps_tabs_left_and_help_right() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.home.open = false;
|
||||
h.app.custom_titlebar = true;
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap();
|
||||
h.call("ui.resize", json!({"width":1400,"height":720}));
|
||||
h.frames(3);
|
||||
for gone in ["appbar:home", "appbar:sun", "appbar:moon", "appbar:save"] {
|
||||
assert!(crate::scenario::handle_at(gone).is_none(), "{gone} is no longer in the title bar");
|
||||
}
|
||||
let at = |name: &str| crate::scenario::handle_at(name).unwrap_or_else(|| panic!("{name}")).x;
|
||||
let [report, _] = h.widget("Report a bug").unwrap();
|
||||
let file = h.widget("File").expect("the File menu has a label");
|
||||
assert_eq!(file[0] as f32, at("appbar:file"));
|
||||
assert!(at("appbar:file") < at("appbar:undo") && at("appbar:undo") < at("appbar:redo"));
|
||||
assert!(at("appbar:redo") < at("tab:home") && at("tab:home") < at("tab:design:0"));
|
||||
assert!((at("tab:design:0") as f64) < report && report < at("appbar:help") as f64);
|
||||
assert!(at("appbar:help") < at("titlebar:preferences"));
|
||||
h.step(&json!({"click":{"handle":"appbar:help"}})).unwrap();
|
||||
assert!(h.app.help.menu, "? opens the Help menu");
|
||||
h.step(&json!({"key":"Escape"})).unwrap();
|
||||
h.step(&json!({"click":{"handle":"tab:home"}})).unwrap();
|
||||
assert!(h.app.home.open, "the Home tab shows the start page");
|
||||
let dark = h.app.ui.dark;
|
||||
h.call("ui.prefs", json!({"theme": if dark { "light" } else { "dark" }}));
|
||||
h.frames(2);
|
||||
assert_ne!(h.app.ui.dark, dark, "the theme is a preference");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_report_button_widens_for_a_longer_translation() {
|
||||
let width_in = |language: &str| {
|
||||
@@ -1312,7 +1369,7 @@ mod header_report_tests {
|
||||
h.frames(3);
|
||||
let center = crate::scenario::handle_at("titlebar:report").expect("the report button is drawn");
|
||||
// The button's right edge is fixed; its width is twice the distance to its centre.
|
||||
2.0 * (1400.0 - crate::titlebar::WIDTH - 90.0 - center.x)
|
||||
2.0 * (1400.0 - crate::titlebar::WIDTH - RIGHT_GROUP - center.x)
|
||||
};
|
||||
let (en, ru) = (width_in("en"), width_in("ru"));
|
||||
assert!((en - 124.0).abs() < 0.5, "{en}");
|
||||
@@ -1345,8 +1402,8 @@ mod header_report_tests {
|
||||
}
|
||||
h.step(&json!({"widget":"Close draft"})).unwrap();
|
||||
assert!(h.app.issue_report.draft.is_none(), "width {width}: review closes through its actual button");
|
||||
h.step(&json!({"click":{"handle":"appbar:home"}})).unwrap();
|
||||
assert!(h.app.home.open, "width {width}: Home remains reachable beside Report");
|
||||
h.step(&json!({"click":{"handle":"tab:home"}})).unwrap();
|
||||
assert!(h.app.home.open, "width {width}: the Home tab remains reachable beside Report");
|
||||
assert_eq!(before, (h.app.session.doc.to_json(), h.app.session.undo.len(), h.app.session.revision));
|
||||
}
|
||||
}
|
||||
@@ -1392,7 +1449,7 @@ mod header_report_tests {
|
||||
mod home_tests {
|
||||
use super::*;
|
||||
|
||||
/// Home hides the design: its Save, Undo and Redo buttons, the File menu's Insert, Save
|
||||
/// Home hides the design: its Undo and Redo buttons, the File menu's Insert, Save
|
||||
/// and Export, and the design's keyboard shortcuts leave it alone. They work again once
|
||||
/// the design is back.
|
||||
#[test]
|
||||
@@ -1434,7 +1491,7 @@ mod home_tests {
|
||||
h.call("ui.click", json!({"x": at.x, "y": at.y}));
|
||||
};
|
||||
let mut actions: Vec<(String, Box<dyn Fn(&mut crate::scenario::Harness)>)> = Vec::new();
|
||||
for icon in ["save", "undo", "redo"] {
|
||||
for icon in ["undo", "redo"] {
|
||||
actions.push((format!("{icon} button"), Box::new(move |h| click(h, &format!("appbar:{icon}")))));
|
||||
}
|
||||
for (key, cmd) in [("S", true), ("Z", true), ("Y", true), ("Delete", false), ("V", false), ("E", false), ("S", false)] {
|
||||
@@ -1473,8 +1530,9 @@ mod home_tests {
|
||||
h.call("ui.key", json!({"key": "Z", "cmd": true}));
|
||||
h.settle();
|
||||
assert_eq!(h.app.session.redo.len(), 1, "Ctrl+Z works once Home is closed");
|
||||
let at = crate::scenario::handle_at("appbar:save").unwrap();
|
||||
h.call("ui.click", json!({"x": at.x, "y": at.y}));
|
||||
h.ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_file_menu"), false));
|
||||
click(&mut h, "appbar:file");
|
||||
h.step(&json!({"widget": "Save"})).unwrap();
|
||||
h.settle();
|
||||
assert!(h.app.session.path.is_some() && !h.app.session.is_dirty(), "the design saves once Home is closed");
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||

|
||||
|
||||
- **Title bar.** On Windows and Linux the application bar is the window's title bar: File, save,
|
||||
undo and redo, the start page, the theme, Help, and one tab per open design (each with its own
|
||||
undo history, selection and camera).
|
||||
- **Title bar.** On Windows and Linux the application bar is the window's title bar. On the left:
|
||||
the File menu (New, Open, Save, Save As, Export…), undo and redo, the Home tab (the start page)
|
||||
and one tab per open design (each with its own undo history, selection and camera; a dot marks
|
||||
unsaved changes). On the right: Report a bug, Help (?), Preferences (which hold the theme) and the
|
||||
window's buttons.
|
||||
- **Toolbar.** The workspace switcher (DESIGN) sits on the left. The tabs (SOLID, SURFACE,
|
||||
SHEET METAL, PLASTIC, UTILITIES, and SKETCH while you sketch) hold panels of commands. Click a
|
||||
panel's name for all its commands. Drag a command from the dropdown onto a panel's label
|
||||
|
||||
Reference in new issue
Block a user