Compare commits

...
3 changed files with 127 additions and 43 deletions

No files matched your search

+6 -4
View File
@@ -19,10 +19,12 @@ fn app() -> PhotocraftApp {
}
fn harness((w, h, scale): (f32, f32, f32)) -> Harness<'static, PhotocraftApp> {
let mut h = Harness::builder()
.with_size(egui::vec2(w / scale, h / scale))
.with_pixels_per_point(scale)
.build_ui_state(|ui, app| crate::menus::menu_bar(app, ui), app());
let mut h = Harness::builder().with_size(egui::vec2(w / scale, h / scale)).with_pixels_per_point(scale).build_ui_state(
|ui, app| {
crate::menus::menu_bar(app, ui);
},
app(),
);
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
h.ctx.all_styles_mut(|s| s.scroll_animation = egui::style::ScrollAnimation::none());
h.run_steps(3);
+35 -6
View File
@@ -694,8 +694,10 @@ fn menu_tint(name: &str) -> Option<egui::Color32> {
})
}
pub fn menu_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
/// Draws the menu bar; returns the right edge of the last menu title (the bar itself fills the row).
pub fn menu_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) -> f32 {
let items = menu_items(app);
let mut right = ui.cursor().left();
let mut clicked: Option<String> = None;
let t = crate::theme::Tokens::get(ui.ctx());
let mut nav = crate::menu_nav::Nav::load(ui.ctx());
@@ -721,6 +723,7 @@ pub fn menu_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
});
buttons.push(r.response);
}
right = buttons.iter().map(|b| b.rect.right()).fold(right, f32::max);
switch_on_hover(ui.ctx(), &buttons);
let tops: Vec<egui::Id> = buttons.iter().map(egui::Popup::default_response_id).collect();
nav.keys(ui.ctx(), &tops, &mut clicked);
@@ -731,6 +734,7 @@ pub fn menu_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
let ctx = ui.ctx().clone();
let _ = invoke(app, &ctx, &id, json!({}));
}
right
}
/// True only when the pointer can actually reach a menu title. A tall submenu can be
@@ -863,7 +867,12 @@ mod tests {
for theme in crate::theme::ThemeKind::ALL {
for width in [640.0, 1440.0] {
let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
let mut harness = Harness::builder().with_size(egui::vec2(width, 200.0)).build_ui_state(|ui, app| menu_bar(app, ui), app);
let mut harness = Harness::builder().with_size(egui::vec2(width, 200.0)).build_ui_state(
|ui, app| {
menu_bar(app, ui);
},
app,
);
PhotocraftApp::setup_context(&harness.ctx, theme);
harness.run_steps(3);
@@ -917,7 +926,12 @@ mod tests {
use egui_kittest::{Harness, kittest::Queryable};
let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
let mut harness = Harness::builder().with_size(egui::vec2(1200.0, 700.0)).build_ui_state(|ui, app| menu_bar(app, ui), app);
let mut harness = Harness::builder().with_size(egui::vec2(1200.0, 700.0)).build_ui_state(
|ui, app| {
menu_bar(app, ui);
},
app,
);
PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
harness.run_steps(3);
// Nothing open: hovering a title does not open it.
@@ -939,7 +953,12 @@ mod tests {
for theme in crate::theme::ThemeKind::ALL {
let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
let mut harness = Harness::builder().with_size(egui::vec2(1200.0, 900.0)).build_ui_state(|ui, app| menu_bar(app, ui), app);
let mut harness = Harness::builder().with_size(egui::vec2(1200.0, 900.0)).build_ui_state(
|ui, app| {
menu_bar(app, ui);
},
app,
);
PhotocraftApp::setup_context(&harness.ctx, theme);
harness.run_steps(3);
harness.get_by_label("File").click();
@@ -957,7 +976,12 @@ mod tests {
use egui_kittest::Harness;
let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
let mut harness = Harness::builder().with_size(egui::vec2(900.0, 240.0)).build_ui_state(|ui, app| menu_bar(app, ui), app);
let mut harness = Harness::builder().with_size(egui::vec2(900.0, 240.0)).build_ui_state(
|ui, app| {
menu_bar(app, ui);
},
app,
);
PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
harness.run_steps(3);
// Opening a top-level menu with many entries must not grow its popup past the viewport.
@@ -980,7 +1004,12 @@ mod tests {
use egui_kittest::{Harness, kittest::Queryable};
let app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
let mut harness = Harness::builder().with_size(egui::vec2(900.0, 220.0)).build_ui_state(|ui, app| menu_bar(app, ui), app);
let mut harness = Harness::builder().with_size(egui::vec2(900.0, 220.0)).build_ui_state(
|ui, app| {
menu_bar(app, ui);
},
app,
);
PhotocraftApp::setup_context(&harness.ctx, crate::theme::ThemeKind::ALL[0]);
harness.run_steps(3);
harness.get_by_label("Filter").click();
+86 -33
View File
@@ -292,44 +292,97 @@ pub fn title_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Maximized(!max));
}
let title = app.session.active().map(|d| format!("{}{}", d.doc.name, if d.is_dirty() { " •" } else { "" })).unwrap_or_else(|| "PhotoCraft".into());
ui.painter().text(full.center(), Align2::CENTER_CENTER, title, theme::medium(13.0), t.text_dim);
// The menus and the right-hand controls are laid out first; the title is centred in
// whatever room is left between them, shortened or dropped rather than drawn over them.
let (mut menus_right, mut controls_left) = (full.left(), full.right());
ui.horizontal_centered(|ui| {
crate::menus::menu_bar(app, ui);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing.x = 6.0;
let mut ws = app.ui.workspace.clone();
let opts = [
("Essentials".to_string(), "Essentials"),
("Photography".to_string(), "Photography"),
("Painting".to_string(), "Painting"),
("Graphic and Web".to_string(), "Graphic and Web"),
];
if widgets::dropdown(ui, "workspace", &mut ws, &opts, 130.0) {
app.ui.workspace = ws;
crate::menus::apply_workspace(app);
}
if icons::button(ui, "search", 28.0, app.ui.palette_open, "Search commands (⌘K)").clicked() {
app.ui.palette_open = !app.ui.palette_open;
}
let theme_icon = if t.dark() { "sun" } else { "moon" };
if icons::button(ui, theme_icon, 28.0, false, "Switch theme").clicked() {
let next = app.ui.theme.next();
app.set_theme(ui.ctx(), next);
}
// Always one click away: the community Discord.
let discord = egui::Button::image_and_text(
icons::image("message-square", 14.0, t.text_dim),
egui::RichText::new("Discord").color(t.text_dim).size(12.0),
)
.frame(false);
if ui.add(discord).on_hover_text(format!("Join the ArtCraft Discord ({})", crate::links::DISCORD)).clicked() {
crate::links::open(app, ui.ctx(), crate::links::DISCORD);
}
});
menus_right = crate::menus::menu_bar(app, ui);
controls_left = ui
.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing.x = 6.0;
let mut ws = app.ui.workspace.clone();
let opts = [
("Essentials".to_string(), "Essentials"),
("Photography".to_string(), "Photography"),
("Painting".to_string(), "Painting"),
("Graphic and Web".to_string(), "Graphic and Web"),
];
if widgets::dropdown(ui, "workspace", &mut ws, &opts, 130.0) {
app.ui.workspace = ws;
crate::menus::apply_workspace(app);
}
if icons::button(ui, "search", 28.0, app.ui.palette_open, "Search commands (⌘K)").clicked() {
app.ui.palette_open = !app.ui.palette_open;
}
let theme_icon = if t.dark() { "sun" } else { "moon" };
if icons::button(ui, theme_icon, 28.0, false, "Switch theme").clicked() {
let next = app.ui.theme.next();
app.set_theme(ui.ctx(), next);
}
// Always one click away: the community Discord.
let discord = egui::Button::image_and_text(
icons::image("message-square", 14.0, t.text_dim),
egui::RichText::new("Discord").color(t.text_dim).size(12.0),
)
.frame(false);
if ui.add(discord).on_hover_text(format!("Join the ArtCraft Discord ({})", crate::links::DISCORD)).clicked() {
crate::links::open(app, ui.ctx(), crate::links::DISCORD);
}
ui.min_rect().left()
})
.inner;
});
let font = theme::medium(13.0);
let galley = ui.painter().layout_no_wrap(title.clone(), font.clone(), t.text_dim);
if let Some(x) = title_x(full.center().x, menus_right, controls_left, galley.size().x) {
ui.painter().galley(egui::pos2(x, full.center().y - galley.size().y / 2.0), galley, t.text_dim);
} else if controls_left - menus_right > 80.0 {
// Too narrow for the whole name: show the start of it, elided, in the free gap.
let avail = controls_left - menus_right - 2.0 * TITLE_GAP;
let mut job = egui::text::LayoutJob::simple_singleline(title, font, t.text_dim);
job.wrap = egui::text::TextWrapping::truncate_at_width(avail);
let g = ui.painter().layout_job(job);
ui.painter().galley(egui::pos2(menus_right + TITLE_GAP, full.center().y - g.size().y / 2.0), g, t.text_dim);
}
});
}
/// Minimum space kept between the window title and the menus or controls beside it.
const TITLE_GAP: f32 = 16.0;
/// Left edge for a title `width` wide: centred on `center` when it fits between the menus
/// (ending at `menus_right`) and the right-hand controls (starting at `controls_left`), else
/// slid into that gap; None when the gap is too small for the whole title.
fn title_x(center: f32, menus_right: f32, controls_left: f32, width: f32) -> Option<f32> {
let (lo, hi) = (menus_right + TITLE_GAP, controls_left - TITLE_GAP - width);
if !(lo.is_finite() && hi.is_finite() && width.is_finite()) || hi < lo {
return None;
}
if !center.is_finite() {
return Some(lo);
}
Some((center - width / 2.0).clamp(lo, hi))
}
#[cfg(test)]
mod title_tests {
use super::title_x;
#[test]
fn title_never_overlaps_menus_or_controls() {
// Wide window: centred.
assert_eq!(title_x(800.0, 420.0, 1300.0, 60.0), Some(770.0));
// Centre would hit the menus: slid right into the gap.
assert_eq!(title_x(400.0, 420.0, 1300.0, 60.0), Some(436.0));
// Centre would hit the controls: slid left.
assert_eq!(title_x(1280.0, 420.0, 1300.0, 60.0), Some(1224.0));
// No room for the whole title: the caller elides or drops it.
assert_eq!(title_x(400.0, 420.0, 480.0, 60.0), None);
assert_eq!(title_x(f32::NAN, 420.0, 1300.0, 60.0), Some(436.0));
assert_eq!(title_x(400.0, f32::INFINITY, 1300.0, 60.0), None);
}
}
// ----------------------------------------------------------------------------- options bar
pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {