mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-17 12:46:57 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
122931932a |
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user