Compare commits

...
Author SHA1 Message Date
MarekSpodBiedry 4b3d448715 Edit › Toolbar: hidden tools leave the toolbar (#1116) 2026-10-08 23:24:12 +00:00
+83 -9
View File
@@ -51,12 +51,38 @@ fn slot_tool(ui: &egui::Ui, current: Tool, slot: &[Tool], key: egui::Id) -> Tool
ui.data(|d| d.get_temp::<Tool>(key)).filter(|t| slot.contains(t)).unwrap_or(slot[0])
}
/// [`TOOL_SECTIONS`] without the tools hidden in Edit › Toolbar (`hidden` holds tool names: the
/// dialog writes `Tool` debug names, `edit.toolbar` takes any name `Tool::from_name` reads). A
/// slot keeps only its visible tools, so its flyout lists just those; a slot with none left is
/// dropped, and so is a section with no slots. Each slot carries its index in `TOOL_SECTIONS`,
/// so its remembered tool and flyout stay put when other slots are hidden.
fn visible_sections(hidden: &[String]) -> Vec<Vec<(usize, Vec<Tool>)>> {
let hidden: Vec<Tool> = hidden.iter().map(String::as_str).filter_map(Tool::from_name).collect();
let mut index = 0usize;
let mut sections = Vec::new();
for section in TOOL_SECTIONS {
let mut slots = Vec::new();
for slot in section.iter() {
let tools: Vec<Tool> = slot.iter().copied().filter(|tool| !hidden.contains(tool)).collect();
if !tools.is_empty() {
slots.push((index, tools));
}
index += 1;
}
if !slots.is_empty() {
sections.push(slots);
}
}
sections
}
pub fn toolbar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
let (w1, bx, m) = if t.pro { (40.0, 30.0, 5i8) } else { (50.0, 36.0, 7i8) };
let sections = visible_sections(&app.session.prefs().toolbar.hidden);
// Two columns when the header chevron asks for them, or when one column doesn't fit.
let slots: usize = TOOL_SECTIONS.iter().map(|g| g.len()).sum();
let double = app.ui.panels.toolbar_double || toolbar_needs_double(slots, TOOL_SECTIONS.len(), bx, t.pro, ui.available_rect_before_wrap().height());
let slots: usize = sections.iter().map(Vec::len).sum();
let double = app.ui.panels.toolbar_double || toolbar_needs_double(slots, sections.len(), bx, t.pro, ui.available_rect_before_wrap().height());
let w = if double { w1 + bx + 2.0 } else { w1 };
egui::Panel::left("toolbar").resizable(false).exact_size(w).frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin::symmetric(m, 8))).show(
ui,
@@ -94,10 +120,8 @@ pub fn toolbar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
if ui.input(|i| i.pointer.any_pressed()) {
ui.data_mut(|d| d.remove::<egui::Id>(held_id));
}
let mut slot_index = 0usize;
// Pro has no group dividers, so its two columns fill every row across groups.
let flat: Vec<&[Tool]> = TOOL_SECTIONS.iter().flat_map(|s| s.iter().copied()).collect();
let sections: Vec<&[&[Tool]]> = if t.pro { vec![&flat[..]] } else { TOOL_SECTIONS.to_vec() };
let sections: Vec<Vec<(usize, Vec<Tool>)>> = if t.pro { vec![sections.into_iter().flatten().collect()] } else { sections };
for (si, section) in sections.iter().enumerate() {
// Photoshop 2026 draws one uninterrupted column (no group dividers).
if si > 0 && !t.pro {
@@ -106,12 +130,11 @@ pub fn toolbar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
ui.painter().line_segment([r.left_center() + vec2(8.0, 0.0), r.right_center() - vec2(8.0, 0.0)], Stroke::new(1.0, t.separator));
ui.add_space(4.0);
}
let rows: Vec<&[&[Tool]]> = if double { section.chunks(2).collect() } else { section.chunks(1).collect() };
let rows: Vec<&[(usize, Vec<Tool>)]> = if double { section.chunks(2).collect() } else { section.chunks(1).collect() };
for row in rows {
ui.horizontal(|ui| {
for slot in row.iter() {
let key = egui::Id::new(("tool-slot", slot_index));
slot_index += 1;
for (slot_index, slot) in row.iter() {
let key = egui::Id::new(("tool-slot", *slot_index));
let tool = slot_tool(ui, app.ui.tool, slot, key);
let sel = slot.contains(&app.ui.tool);
let tip = if tool.key() == '\0' { tl!(tool.label()).to_string() } else { format!("{} ({})", tl!(tool.label()), tool.key()) };
@@ -3230,6 +3253,57 @@ mod type_flyout_tests {
}
}
#[cfg(test)]
mod toolbar_hidden_tests {
use super::*;
/// Clickable tool-sized buttons the toolbar drew in its last pass.
fn tool_buttons(app: &mut PhotocraftApp) -> usize {
let ctx = egui::Context::default();
PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::ALL[0]);
for time in [0.0, 0.1] {
let mut out = ctx.run_ui(
egui::RawInput { time: Some(time), screen_rect: Some(Rect::from_min_size(egui::Pos2::ZERO, vec2(1200.0, 1800.0))), ..Default::default() },
|ui| toolbar(app, ui),
);
out.textures_delta.clear();
}
let size = Vec2::splat(if Tokens::get(&ctx).pro { 30.0 } else { 36.0 });
ctx.viewport(|v| v.prev_pass.widgets.layers().flat_map(|(_, w)| w.iter()).filter(|w| w.rect.size() == size && w.sense.senses_click()).count())
}
fn hide(app: &mut PhotocraftApp, tools: &[Tool]) {
let names: Vec<String> = tools.iter().map(|tool| format!("{tool:?}")).collect();
app.session.edit_prefs(|p| p.toolbar.hidden = names);
}
#[test]
fn hidden_tools_leave_the_toolbar() {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
let all = tool_buttons(&mut app);
let slots: usize = TOOL_SECTIONS.iter().map(|section| section.len()).sum();
hide(&mut app, &Tool::ALL);
assert_eq!(tool_buttons(&mut app), all - slots, "every tool slot is gone when every tool is hidden");
hide(&mut app, &[Tool::Brush, Tool::Pencil, Tool::MixerBrush]);
assert_eq!(tool_buttons(&mut app), all - 1, "a slot whose tools are all hidden is gone");
hide(&mut app, &[Tool::Sponge]);
assert_eq!(tool_buttons(&mut app), all, "a slot with a visible tool stays");
}
#[test]
fn a_slot_keeps_only_its_visible_tools_and_its_index() {
let sections = visible_sections(&["Sponge".to_string(), "Move".to_string()]);
assert_eq!(sections.len(), TOOL_SECTIONS.len() - 1, "the Move section has no tools left");
let index = TOOL_SECTIONS.iter().flat_map(|section| section.iter()).position(|slot| slot.contains(&Tool::Sponge)).unwrap();
let slot = sections.iter().flatten().find(|(i, _)| *i == index).map(|(_, tools)| tools.clone());
assert_eq!(slot, Some(vec![Tool::Dodge, Tool::Burn]));
assert!(sections.iter().flatten().all(|(_, tools)| !tools.contains(&Tool::Move) && !tools.contains(&Tool::Sponge)));
assert_eq!(visible_sections(&[]).iter().map(Vec::len).sum::<usize>(), TOOL_SECTIONS.iter().map(|section| section.len()).sum::<usize>());
let by_command = visible_sections(&["sponge".to_string(), "move".to_string()]);
assert_eq!(by_command, sections, "edit.toolbar names are read like `ui.set {{tool}}` names");
}
}
#[cfg(test)]
mod properties_card_tests {
use super::*;