diff --git a/crates/ui-egui/src/edit_toolbar.rs b/crates/ui-egui/src/edit_toolbar.rs index cad227ac..1138e37b 100644 --- a/crates/ui-egui/src/edit_toolbar.rs +++ b/crates/ui-egui/src/edit_toolbar.rs @@ -408,7 +408,10 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { let id = drawer_id(); let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(WIDTH, 200.0), |r| r.size()); let x = (bar.right() + GAP).min(screen.right() - size.x - GAP).max(screen.left()); - let y = button.top().min(screen.bottom() - size.y - GAP).max(screen.top() + GAP); + // Never above the toolbar's top: higher up are the window's title bar (and on macOS its + // buttons) and the bars under it. + let top = bar.top().max(screen.top() + GAP); + let y = button.top().min(screen.bottom() - size.y - GAP).max(top); let in_toolbar: BTreeSet<&str> = crate::toolbar::slots(app).into_iter().flat_map(|(_, s)| s).collect(); let active = app.session.tool_id(); let dragged = DragAndDrop::payload::(ctx).map(|d| *d); @@ -448,7 +451,7 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { ui.add_space(4.0); // As tall as its tools, up to the window's height (it scrolls then); the Area's size // from the last frame mustn't cap it. - let room = (screen.height() - 2.0 * GAP - 104.0).max(120.0); + let room = (screen.bottom() - top - GAP - 104.0).max(120.0); egui::ScrollArea::vertical().max_height(room).min_scrolled_height(room).auto_shrink([false, true]).show(ui, |ui| { ui.spacing_mut().item_spacing = vec2(0.0, 0.0); let inner = ui.available_width() - 10.0; @@ -885,6 +888,7 @@ mod tests { let bar = ctx.data(|d| d.get_temp::(toolbar_id())).unwrap(); assert!((drawer.left() - bar.right() - GAP).abs() < 1.0, "beside the toolbar: {drawer:?} {bar:?}"); assert!((drawer.top() - edit.top()).abs() < 1.0 || drawer.bottom() >= SCREEN.y - GAP - 1.0, "by the button: {drawer:?} {edit:?}"); + assert!(drawer.top() >= bar.top() - 0.5, "never above the toolbar (the title bar's there): {drawer:?} {bar:?}"); assert!(drawer.y_range().contains(edit.center().y), "by the button: {drawer:?} {edit:?}"); assert!(drawer.width() < WIDTH + 24.0, "one row of tools wide: {drawer:?}"); // A click on a toolbar tool picks it and leaves the drawer open. diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index f8fd5f77..ae91e998 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -182,9 +182,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option, egui::Rect)> = None; // A group torn off into a floating flyout is raised instead of opening its flyout. let mut raise: Option<&'static str> = None; + let labels = app.session.prefs.tool_group_labels; let mut i = 0; while i < all.len() { - if all[i].0.is_some() && !app.session.prefs.tool_group_labels { + if all[i].0.is_some() && !labels && cols == 2 { + // Without labels, two columns run on from group to group, as the reference's + // Basic toolbar: no half-empty rows, no dashes between rows. + } else if all[i].0.is_some() && !labels { // User Interface › Show Tool Group Labels off: a faint dash between the groups. let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 9.0), Sense::hover()); ui.painter().line_segment([r.center() - vec2(6.0, 0.0), r.center() + vec2(6.0, 0.0)], egui::Stroke::new(1.0, t.divider)); @@ -196,9 +200,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option 6 { format!("{}...", cat.chars().take(4).collect::()) } else { cat.to_string() }; ui.painter().text(r.center() + vec2(0.0, 2.0), egui::Align2::CENTER_CENTER, label, egui::FontId::proportional(11.0), t.text); } - // One row = `cols` slots (a category label always starts a new row). + // One row = `cols` slots (a category label always starts a new row; without labels, a + // new group does only in one column). let mut row = vec![i]; - while row.len() < cols && i + row.len() < all.len() && all[i + row.len()].0.is_none() { + while row.len() < cols && i + row.len() < all.len() && (all[i + row.len()].0.is_none() || !labels) { row.push(i + row.len()); } ui.horizontal(|ui| { @@ -1259,6 +1264,37 @@ pub(crate) mod tests { lefts.len() } + /// Rows of tool buttons by their tops: how many buttons each row holds, top to bottom. + fn row_sizes(rects: &[egui::Rect]) -> Vec { + let mut rows: Vec<(i32, usize)> = vec![]; + for r in rects { + let top = r.top().round() as i32; + match rows.last_mut() { + Some((t, n)) if *t == top => *n += 1, + _ => rows.push((top, 1)), + } + } + rows.into_iter().map(|(_, n)| n).collect() + } + + /// #1736: without group labels (as it ships), two columns run on from group to group, every + /// row full but maybe the last; with the labels, each group starts its own row under its name. + #[test] + fn two_columns_run_on_without_group_labels() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.ui.toolbar_double = true; + let ctx = egui::Context::default(); + let rows = row_sizes(&frame_in(&mut app, &ctx, 0.0, vec![], 1600.0)); + let slots = slots(&app).len(); + assert_eq!(rows.iter().sum::(), slots); + assert!(rows[..rows.len() - 1].iter().all(|n| *n == 2), "{rows:?}"); + assert_eq!(rows.len(), slots.div_ceil(2)); + app.run("prefs.set", json!({"key": "toolGroupLabels", "value": true})).unwrap(); + let labelled = row_sizes(&frame_in(&mut app, &ctx, 1.0, vec![], 1600.0)); + assert!(labelled.len() > rows.len() && labelled.contains(&1), "{labelled:?}"); + } + #[test] fn the_double_arrow_toggles_one_and_two_columns_in_a_short_window() { let mut app = VectorcraftApp::new(Session::new(), Default::default());