diff --git a/crates/ui-egui/src/brand.rs b/crates/ui-egui/src/brand.rs index 0a528569..461af137 100644 --- a/crates/ui-egui/src/brand.rs +++ b/crates/ui-egui/src/brand.rs @@ -1,6 +1,6 @@ //! The VectorCraft brand mark: the app icon (the engraved dragon, `assets/app-icon/`, see its //! README). The PNG carries the icon's rounded corners; it is decoded once per context into a -//! mipmapped texture, so it stays crisp from the 22 pt app-bar mark to the About box. +//! mipmapped texture, so it stays crisp from the 18 pt app-bar mark to the About box. use egui::{Color32, Context, Id, Rect, TextureHandle, TextureOptions, Ui, pos2}; @@ -81,7 +81,7 @@ mod tests { let (uploads, marks) = frame(); assert_eq!(uploads, 1); assert_eq!(marks.len(), 1); - assert_eq!(marks[0].size(), egui::vec2(22.0, 22.0)); + assert_eq!(marks[0].size(), egui::vec2(18.0, 18.0)); // Later frames reuse the texture. let (uploads, marks) = frame(); assert_eq!((uploads, marks.len()), (0, 1)); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 653f24fb..a8c8b15e 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; -use egui::{CornerRadius, Sense, Stroke, StrokeKind, Ui, vec2}; +use egui::{CornerRadius, Sense, Stroke, Ui, vec2}; use serde_json::json; use vectorcraft_doc::NodeKind; @@ -12,8 +12,12 @@ use crate::theme::{self, Tokens}; use crate::widgets; use crate::{VectorcraftApp, icons, menus, titlebar}; -/// The application bar's height, in points. -pub const APP_BAR_HEIGHT: f32 = 44.0; +/// The application bar's height, in points (compact, as in PhotoCraft). +pub const APP_BAR_HEIGHT: f32 = 32.0; +/// The brand mark's side. +const MARK: f32 = 18.0; +/// Height of the workspace switcher. +const WIDGET_H: f32 = 22.0; /// The application bar: brand mark, Home, menus, then Discord, search and the workspace switcher /// at the right. With [`VectorcraftApp::custom_titlebar`] it is also the window's title bar @@ -33,12 +37,12 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal_centered(|ui| { // With the system title bar the OS draws its own icon: no in-app brand mark. if !system { - let (r, _) = ui.allocate_exact_size(vec2(22.0, 22.0), Sense::hover()); + let (r, _) = ui.allocate_exact_size(vec2(MARK, MARK), Sense::hover()); crate::brand::paint_mark(ui, r); - ui.add_space(4.0); + ui.add_space(6.0); } let on_home = menus::home_showing(app); - if widgets::icon_button(ui, "house", tl!("Home"), on_home, 24.0).clicked() { + if widgets::icon_button(ui, "house", tl!("Home"), on_home, 20.0).clicked() { app.run("app.home", json!({})).ok(); } ui.add_space(2.0); @@ -51,8 +55,8 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { menus::menu_bar(app, ui) }; // The right-side group fills the space after the menus from the right; when it runs - // short, Discord goes first (it is also under Help), then the search box becomes an - // icon, then the workspace switcher narrows. + // short, Discord goes first (it is also under Help), then the search icon (the palette + // stays under its shortcut), then the workspace switcher narrows. let full = ui.max_rect(); let right_edge = if custom { full.right() - titlebar::WIDTH - 10.0 } else { full.right() }; let right = egui::Rect::from_min_max(egui::pos2(menus_end + 8.0, full.top()), egui::pos2(right_edge, full.bottom())); @@ -63,45 +67,31 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let ws = ui.painter().layout_no_wrap(ws_name, egui::FontId::proportional(12.0), t.text); let ws_w = (ws.size().x + 36.0).clamp(112.0, 190.0); let gap = ui.spacing().item_spacing.x; - let with_search = ws_w + 8.0 + gap + 200.0; - let search_full = room >= with_search; - let discord = room >= with_search + 10.0 + gap + crate::community::discord_width(ui, false); - let ws_w = if search_full { ws_w } else { ws_w.min(room - 8.0 - gap - 24.0).max(64.0) }; + // The search icon, as in PhotoCraft: shown while a minimal switcher fits beside it. + let icon = 28.0; + let search = room >= 64.0 + 8.0 + gap + icon; + let ws_w = ws_w.min(room - 8.0 - gap - if search { icon } else { 0.0 }).max(64.0); + let discord = search && room >= ws_w + 8.0 + gap + icon + 10.0 + gap + crate::community::discord_width(ui, false); let mut rui = ui.new_child(egui::UiBuilder::new().max_rect(right).layout(egui::Layout::right_to_left(egui::Align::Center))); let ui = &mut rui; // Workspace switcher: shows the current workspace, opens Window → Workspace. - let (wr, wresp) = ui.allocate_exact_size(vec2(ws_w, 24.0), Sense::click()); + let (wr, wresp) = ui.allocate_exact_size(vec2(ws_w, WIDGET_H), Sense::click()); ui.painter().rect_filled(wr, CornerRadius::same(4), if wresp.hovered() { t.hover } else { t.panel }); ui.painter().with_clip_rect(wr.shrink2(vec2(4.0, 0.0))).galley(wr.left_center() + vec2(10.0, -ws.size().y / 2.0), ws, t.text); icons::paint(ui, "chevron-down", egui::Rect::from_center_size(wr.right_center() - vec2(12.0, 0.0), vec2(12.0, 12.0)), t.text_dim); let wresp = wresp.on_hover_text(tl!("Switch workspace")); egui::Popup::menu(&wresp).show(|ui| crate::workspaces::popup(app, ui)); ui.add_space(8.0); - // Search box → command palette. - let open_palette = if search_full { - let (r, resp) = ui.allocate_exact_size(vec2(200.0, 24.0), Sense::click()); - ui.painter().rect_filled(r, CornerRadius::same(12), t.input); - ui.painter().rect_stroke( - r, - CornerRadius::same(12), - Stroke::new(1.0, if resp.hovered() { t.input_border } else { t.divider }), - StrokeKind::Inside, - ); - icons::paint(ui, "search", egui::Rect::from_center_size(r.left_center() + vec2(14.0, 0.0), vec2(13.0, 13.0)), t.text_dim); - ui.painter().text( - r.left_center() + vec2(26.0, 0.0), - egui::Align2::LEFT_CENTER, - tl!("Search commands and tools"), - egui::FontId::proportional(11.5), - t.text_dim, - ); - resp.clicked() - } else { - widgets::icon_button(ui, "search", tl!("Search commands and tools"), false, 24.0).clicked() - }; - if open_palette { - app.ui.palette_open = true; - app.ui.palette_query.clear(); + // Search → command palette (its shortcut opens it too). + if search { + let tip = match menus::shortcut_of("help.commandPalette") { + Some(sc) => format!("{} ({})", tl!("Search commands and tools"), menus::pretty_shortcut(sc)), + None => tl!("Search commands and tools").to_string(), + }; + if widgets::icon_button(ui, "search", &tip, app.ui.palette_open, icon).clicked() { + app.ui.palette_open = !app.ui.palette_open; + app.ui.palette_query.clear(); + } } if discord { ui.add_space(10.0); diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs index d9b9f11c..79b00c53 100644 --- a/crates/ui-egui/src/titlebar.rs +++ b/crates/ui-egui/src/titlebar.rs @@ -248,7 +248,7 @@ mod tests { .layers() .filter(|(l, _)| l.order != Order::Foreground) .flat_map(|(_, ws)| ws.iter()) - .filter(|w| w.sense.senses_click() && w.rect.top() < 44.0 && !skip.contains(&w.id)) + .filter(|w| w.sense.senses_click() && w.rect.top() < 32.0 && !skip.contains(&w.id)) .map(|w| w.rect) .collect() }) @@ -263,7 +263,7 @@ mod tests { let out = w.frame(vec![]); let [min, max, close] = CAPTIONS.map(|c| w.caption(c)); assert_eq!((close.right(), close.top()), (width, 0.0), "Close sits in the window's corner at {width}"); - assert!(close.height() >= 42.0 && [min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH)); + assert!(close.height() >= 30.0 && [min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH)); assert_eq!((min.right(), max.right()), (max.left(), close.left())); let widgets = w.bar_widgets(); assert!(widgets.len() >= 11, "home, eight menus, workspace switcher, search: {widgets:?}"); @@ -273,10 +273,12 @@ mod tests { assert!(!a.shrink(0.5).intersects(b.shrink(0.5)), "{a:?} overlaps {b:?} at {width}"); } } - // Wide windows show the whole group; narrow ones drop Discord and shorten the search. + // Wide windows show the whole group; narrow ones drop Discord, then the search icon. let texts: Vec = texts(&out).into_iter().map(|(t, _)| t).collect(); let has = |s: &str| texts.iter().any(|t| t == s); - assert_eq!((has("Discord"), has("Search commands and tools")), (width > 1000.0, width > 1000.0), "{width}: {texts:?}"); + assert_eq!(has("Discord"), width > 1000.0, "{width}: {texts:?}"); + // Search is an icon only (as in PhotoCraft): its label lives in the tooltip. + assert!(!has("Search commands and tools"), "{width}: {texts:?}"); } } @@ -314,7 +316,7 @@ mod tests { fn empty_bar_space_drags_and_double_click_maximizes() { let mut w = Win::new(1440.0); w.frame(vec![]); - let empty = pos2(700.0, 22.0); + let empty = pos2(700.0, 16.0); assert!(w.bar_widgets().iter().all(|r| !r.contains(empty)), "the probe point must be empty bar space"); 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:?}");