Merge pull request #11322 from Saloouu/fix/toolbar-groups-survive-restart

Toolbar slots and tool keys keep their last tool across launches (#3569)
This commit is contained in:
David Mann
2026-11-07 11:55:58 +01:00
committed by GitHub
13 changed files with 274 additions and 30 deletions
+4
View File
@@ -1296,6 +1296,10 @@ pub struct Preferences {
/// Last selected tool, restored at launch. A tool-name string owned and validated by the
/// shell; null in older files. Kept separate from tool options so neither overwrites the other.
pub selected_tool: Value,
/// The tools used last, most recent first, restored at launch so each toolbar slot shows the
/// tool it was left on and each tool key brings it back, as in Photoshop (#3569). Tool-name
/// strings owned and validated by the shell; null in older files.
pub group_tools: Value,
/// View › Extras, the View › Show items (Pixel Grid, Grid, Guides, …) and the View › Snap To
/// targets that differ from their defaults, saved as the user toggles them and restored at
/// launch, as Photoshop keeps them between sessions (#3762). JSON owned by the shell.
+4 -4
View File
@@ -485,9 +485,9 @@ fn preflight_keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool {
if crate::canvas::cancel_mixer_on_context_change(app, ctx) {
return false;
}
// However the tool was picked (toolbar, flyout, shortcut, automation), its group's key brings
// it back later (#2608).
app.ui.remember_group_tool();
// However the tool was picked (toolbar, flyout, shortcut, automation), its group's key and its
// toolbar slot bring it back later (#2608). A temporary tool is no choice, so it isn't noted.
app.ui.remember_group_tool(crate::tool_memory::selected(app));
crate::canvas::polygon_cancel_stale(app);
// Camera Raw is modal like Photoshop's filter dialog: no application shortcut (Save, Undo,
// tools) runs beneath it, and it handles its own keys (Y, U, O, S). Unlike the other dialogs
@@ -924,7 +924,7 @@ fn select_tool_by_key(app: &mut PhotocraftApp, ctx: &egui::Context, pressed: &im
app.ui.tool = match group.iter().position(|x| *x == app.ui.tool) {
Some(i) if shift_switch && !cycle_shift => group[i],
Some(i) => group[(i + 1) % group.len()],
None => app.ui.group_tool(t.key()).filter(|last| group.contains(last)).unwrap_or(group[0]),
None => app.ui.group_tools.iter().copied().find(|last| group.contains(last)).unwrap_or(group[0]),
};
crate::tool_memory::ended_by_tool(app);
// `consume_key` ignores extra ⇧ and ⌥: a ⇧/⌥ switch is never spring-loaded.
@@ -72,6 +72,20 @@ fn shift_key_tool_switch_is_the_default() {
assert_ne!(app.ui.tool, Tool::Type);
}
/// A tool with a shortcut of its own leaves its letter (#4086) but stays in the history: the letter
/// falls back on the last tool it still picks, not on its group's first.
#[test]
fn a_tool_key_passes_over_a_remapped_tool_to_the_last_one_it_picks() {
use crate::state::Tool;
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
let ctx = egui::Context::default();
app.run("prefs.set", json!({"path": "shortcuts.tools.select.polygonLasso", "value": "F6"})).unwrap();
app.ui.remember_group_tool(Tool::MagneticLasso);
app.ui.remember_group_tool(Tool::PolygonLasso);
press_key(&mut app, &ctx, Key::L, Modifiers::NONE);
assert_eq!(app.ui.tool, Tool::MagneticLasso);
}
/// #3109: on a German layout Ö / # (physical `;` / `\`) step the brush size and ⇧Ö / ⇧#
/// (⇧# types `'`) its hardness, as Photoshop's German shortcuts.
#[test]
+2 -1
View File
@@ -112,7 +112,8 @@ use title_bar::title_x;
pub use title_bar::{sync_window_title, system_title_bar, title_bar, window_title};
#[cfg(test)]
use toolbar::{
STUDIO_CHIP, TOOL_SECTIONS, chips_height, color_chips, next_slot_tool, tool_flyout_drag_open, tool_flyout_release, toolbar_icon_size, visible_sections,
STUDIO_CHIP, TOOL_SECTIONS, chips_height, color_chips, next_slot_tool, slot_tool, tool_flyout_drag_open, tool_flyout_release, toolbar_icon_size,
visible_sections,
};
pub use toolbar::{paint_toolbar_flyout_indicator, toolbar};
+6 -6
View File
@@ -35,13 +35,13 @@ pub(crate) const TOOL_SECTIONS: &[&[&[Tool]]] = &[
&[&[Tool::Hand, Tool::RotateView], &[Tool::Zoom]],
];
/// The tool a slot shows: the current tool if it belongs to the slot, else the last one used.
fn slot_tool(ui: &egui::Ui, current: Tool, slot: &[Tool], key: egui::Id) -> Tool {
/// The tool a slot shows: the current tool if it belongs to the slot, else the last one used
/// (`group_tools`, most recent first), else the slot's first tool.
pub(super) fn slot_tool(current: Tool, group_tools: &[Tool], slot: &[Tool]) -> Tool {
if slot.contains(&current) {
ui.data_mut(|d| d.insert_temp(key, current));
return current;
}
ui.data(|d| d.get_temp::<Tool>(key)).filter(|t| slot.contains(t)).unwrap_or(slot[0])
group_tools.iter().chain(slot).copied().find(|t| slot.contains(t)).unwrap_or(current)
}
/// Next tool in a toolbar flyout slot after `from`, wrapping (Photoshop Alt/Option-click). An
@@ -61,7 +61,7 @@ pub(crate) fn toolbar_position(tool: Tool) -> usize {
/// 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.
/// so its flyout stays put when other slots are hidden.
pub(super) 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;
@@ -226,7 +226,7 @@ pub fn toolbar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
for (slot_index, slot) in row.iter() {
let key = egui::Id::unique(("tool-slot", *slot_index));
let tool = slot_tool(ui, app.ui.tool, slot, key);
let tool = slot_tool(app.ui.tool, &app.ui.group_tools, slot);
let sel = slot.contains(&app.ui.tool);
let tip = match crate::tool_shortcuts::shortcut(app, tool) {
Some(sc) => format!("{} ({})", tl!(tool.label()), crate::shortcuts::pretty(&sc)),
@@ -450,6 +450,67 @@ fn next_slot_tool_wraps_around_the_flyout() {
assert_eq!(next_slot_tool(&[], Tool::Brush), Tool::Brush);
}
#[test]
fn slot_tool_is_the_current_tool_else_the_slots_last_else_its_first() {
let lasso = [Tool::Lasso, Tool::PolygonLasso, Tool::MagneticLasso];
assert_eq!(slot_tool(Tool::Move, &[], &lasso), Tool::Lasso);
let used = [Tool::Brush, Tool::PolygonLasso, Tool::Lasso];
assert_eq!(slot_tool(Tool::Move, &used, &lasso), Tool::PolygonLasso);
assert_eq!(slot_tool(Tool::MagneticLasso, &used, &lasso), Tool::MagneticLasso);
// Polygonal Lasso hidden in Edit › Toolbar: the slot falls back on the next one used.
assert_eq!(slot_tool(Tool::Move, &used, &[Tool::Lasso, Tool::MagneticLasso]), Tool::Lasso);
// Slots don't line up with keys: Blur's slot and the two single marquees have no key at all,
// and the Hand slot has two.
let used = [Tool::Sharpen, Tool::SingleRowMarquee, Tool::RotateView];
assert_eq!(slot_tool(Tool::Move, &used, &[Tool::Blur, Tool::Sharpen, Tool::Smudge]), Tool::Sharpen);
let marquee = [Tool::RectMarquee, Tool::EllipseMarquee, Tool::SingleRowMarquee, Tool::SingleColumnMarquee];
assert_eq!(slot_tool(Tool::Move, &used, &marquee), Tool::SingleRowMarquee);
assert_eq!(slot_tool(Tool::Move, &used, &[Tool::Hand, Tool::RotateView]), Tool::RotateView);
// Empty slot: no panic, the current tool stays.
assert_eq!(slot_tool(Tool::Brush, &used, &[]), Tool::Brush);
}
/// #3569: after a restart a slot is still on the tool it was left on, whichever tool is selected:
/// a click picks that tool, not the slot's first.
#[test]
fn slots_come_back_on_their_last_tool_after_a_restart() {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.session.prefs.edit(|p| p.group_tools = json!(["PolygonLasso", "PaintBucket", "Pencil", "Sharpen", "SingleRowMarquee"]));
crate::tool_memory::restore(&mut app);
let ctx = egui::Context::default();
PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::ALL[0]);
frame(&mut app, &ctx, 0.0, vec![]);
frame(&mut app, &ctx, 0.1, vec![]);
let bx = if Tokens::get(&ctx).pro { 30.0 } else { 36.0 };
let mut buttons: Vec<Rect> = ctx.viewport(|v| {
v.prev_pass
.widgets
.layers()
.flat_map(|(_, w)| w.iter())
.filter(|w| w.rect.size() == egui::Vec2::splat(bx) && w.sense.senses_click())
.map(|w| w.rect)
.collect()
});
buttons.sort_by(|a, b| a.top().total_cmp(&b.top()));
let pointer = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
let slots = [
(Tool::Brush, Tool::Pencil),
(Tool::Gradient, Tool::PaintBucket),
(Tool::Lasso, Tool::PolygonLasso),
(Tool::Blur, Tool::Sharpen),
(Tool::RectMarquee, Tool::SingleRowMarquee),
];
for (i, (first, last)) in slots.into_iter().enumerate() {
let index = TOOL_SECTIONS.iter().flat_map(|section| section.iter()).position(|slot| slot.contains(&first)).unwrap();
let at = buttons[index].center();
let t = 1.0 + i as f64;
frame(&mut app, &ctx, t, vec![egui::Event::PointerMoved(at), pointer(at, true)]);
frame(&mut app, &ctx, t + 0.05, vec![pointer(at, false)]);
frame(&mut app, &ctx, t + 0.1, vec![]);
assert_eq!(app.ui.tool, last);
}
}
/// Edit › Toolbar hidden tools shrink the Alt-click cycle to the remaining flyout members.
#[test]
fn visible_sections_drop_hidden_tools_from_alt_click_slots() {
+1
View File
@@ -53,6 +53,7 @@ pub struct Runtime {
/// Tool options as last remembered in the preferences (see `tool_memory`).
pub(crate) tool_options: Option<crate::state::ToolOptions>,
pub(crate) selected_tool: Option<crate::state::Tool>,
pub(crate) group_tools: Option<Vec<crate::state::Tool>>,
/// The tool owning the session brush and the other tools' brushes, as last remembered.
pub(crate) tool_brushes: Option<crate::tool_memory::ToolBrushes>,
}
+119
View File
@@ -1139,6 +1139,125 @@ fn selected_tool_excludes_temporary_switches() {
assert_eq!(stored(&store)["selectedTool"], json!("PolygonLasso"));
}
/// #3569: a restart keeps the last tool of every group, not only the selected tool's.
#[test]
fn group_tools_survive_a_restart() {
use crate::state::Tool;
let (mut app, store) = app_with_store();
let ctx = egui::Context::default();
tick(&mut app, &ctx);
for tool in [Tool::Brush, Tool::Pencil, Tool::PaintBucket, Tool::PolygonLasso, Tool::Pencil, Tool::Zoom] {
app.ui.tool = tool;
crate::tool_memory::persist(&mut app, &ctx);
tick(&mut app, &ctx);
}
assert_eq!(stored(&store)["groupTools"], json!(["Zoom", "Pencil", "PolygonLasso", "PaintBucket", "Brush", "Move"]), "most recent first, each once");
let (app2, _) = app_with_saved(store.lock().unwrap().clone());
assert_eq!(app2.ui.tool, Tool::Zoom);
assert_eq!(app2.ui.group_tools, app.ui.group_tools);
assert_eq!(app2.ui.group_tool('B'), Some(Tool::Pencil), "the group's latest tool");
assert_eq!(app2.ui.group_tool('G'), Some(Tool::PaintBucket));
assert_eq!(app2.ui.group_tool('L'), Some(Tool::PolygonLasso));
}
#[test]
fn group_tools_are_flushed_on_exit_without_another_frame() {
use crate::state::Tool;
let (mut app, store) = app_with_store();
app.ui.tool = Tool::Pencil;
crate::tool_memory::persist(&mut app, &egui::Context::default());
app.ui.tool = Tool::PaintBucket;
crate::tool_memory::save_on_exit(&mut app).unwrap();
let (app2, _) = app_with_saved(store.lock().unwrap().clone());
assert_eq!(app2.ui.group_tool('B'), Some(Tool::Pencil));
assert_eq!(app2.ui.group_tool('G'), Some(Tool::PaintBucket));
}
/// A frame as far as the tools go: the key handling, then what is remembered.
fn tool_frame(app: &mut PhotocraftApp, ctx: &egui::Context, events: Vec<egui::Event>) {
ctx.begin_pass(egui::RawInput { events, ..Default::default() });
crate::shortcuts::handle(app, ctx);
ctx.end_pass().textures_delta.clear();
crate::tool_memory::persist(app, ctx);
tick(app, ctx);
}
/// A tool picked during a drag that ends on the tool it began with: the selected tool never
/// changed, but that tool's group did.
#[test]
fn group_tools_are_saved_when_the_selected_tool_stays_the_same() {
use crate::state::Tool;
let (mut app, store) = app_with_store();
let ctx = egui::Context::default();
let pointer = |pressed| egui::Event::PointerButton { pos: egui::Pos2::ZERO, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
app.ui.tool = Tool::Brush;
tool_frame(&mut app, &ctx, vec![]);
tool_frame(&mut app, &ctx, vec![pointer(true)]);
app.ui.tool = Tool::PaintBucket;
tool_frame(&mut app, &ctx, vec![]);
app.ui.tool = Tool::Brush;
tool_frame(&mut app, &ctx, vec![]);
tool_frame(&mut app, &ctx, vec![pointer(false)]);
assert_eq!(stored(&store)["selectedTool"], json!("Brush"));
let (app2, _) = app_with_saved(store.lock().unwrap().clone());
assert_eq!(app2.ui.group_tool('G'), Some(Tool::PaintBucket));
}
/// The pen's eraser end and a held tool letter don't change what a group shows or brings back,
/// neither during the session nor when quitting while they are in use.
#[test]
fn group_tools_exclude_temporary_switches() {
use crate::state::Tool;
let (mut app, store) = app_with_store();
let ctx = egui::Context::default();
for tool in [Tool::MagicEraser, Tool::Brush] {
app.ui.tool = tool;
tool_frame(&mut app, &ctx, vec![]);
}
app.stylus.tool_before_eraser = Some(Tool::Brush);
app.ui.tool = Tool::Eraser;
tool_frame(&mut app, &ctx, vec![]);
assert_eq!(app.ui.group_tool('E'), Some(Tool::MagicEraser), "the pen's eraser end");
app.stylus.tool_before_eraser = None;
app.ui.tool = Tool::Brush;
let press = egui::Event::Key { key: egui::Key::Z, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
tool_frame(&mut app, &ctx, vec![press]);
tool_frame(&mut app, &ctx, vec![]);
assert_eq!(app.ui.tool, Tool::Zoom, "Z is still down");
assert_eq!(app.ui.group_tool('Z'), None, "a held letter");
crate::tool_memory::save_on_exit(&mut app).unwrap();
assert_eq!(stored(&store)["groupTools"], json!(["Brush", "MagicEraser", "Move"]));
}
/// A selected tool this version doesn't know starts on the default tool. The saved list stays as
/// it is, names this version doesn't know included, until the next tool is picked.
#[test]
fn a_launch_alone_saves_no_group_tools() {
use crate::state::Tool;
let saved = json!({"selectedTool": "FutureTool", "groupTools": ["Pencil", "FutureTool", "PaintBucket"]});
let (mut app, store) = app_with_saved(Some(saved.to_string()));
let ctx = egui::Context::default();
tool_frame(&mut app, &ctx, vec![]);
assert_eq!(stored(&store)["groupTools"], saved["groupTools"]);
app.ui.tool = Tool::Brush;
tool_frame(&mut app, &ctx, vec![]);
assert_eq!(stored(&store)["groupTools"], json!(["Brush", "Move", "Pencil", "PaintBucket"]));
}
/// A damaged or hand-edited `groupTools` costs no other preference, and what reads back counts.
#[test]
fn damaged_group_tools_keep_what_reads_back() {
use crate::state::Tool;
for bad in [json!(null), json!("Pencil"), json!(42), json!({"B": "Pencil"})] {
let (app, _) = app_with_saved(Some(json!({"groupTools": bad, "toolOptions": {"feather": 12.0}}).to_string()));
assert_eq!(app.ui.group_tools, [Tool::Move], "{bad}");
assert_eq!(app.ui.tool_options.feather, 12.0);
}
let saved = json!({"groupTools": ["FutureTool", "Pencil", 7, null, {}, "PaintBucket", "Pencil"]});
let (app, _) = app_with_saved(Some(saved.to_string()));
assert_eq!(app.ui.group_tools, [Tool::Move, Tool::Pencil, Tool::PaintBucket]);
}
#[test]
fn tool_options_survive_a_restart() {
let (mut app, store) = app_with_store();
+11 -8
View File
@@ -227,8 +227,11 @@ pub struct DocWindow {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct UiState {
pub tool: Tool,
/// The last tool used in each shortcut group (the tools sharing a key, [`Tool::key`]), at most
/// one per group: the group's key brings it back, as in Photoshop (#2608).
/// The tools used so far, most recent first and each one once. The first tool of a shortcut
/// group (the tools sharing a key, [`Tool::key`]) is the one its key brings back, and the first
/// tool of a toolbar slot is the one the slot shows, as in Photoshop (#2608, #3569). One list
/// answers both because they don't line up: the Marquee slot holds tools with and without a
/// key, the Hand slot two keys. Kept between launches in `Preferences::group_tools`.
#[serde(default)]
pub group_tools: Vec<Tool>,
/// A tool letter still held (spring-loaded tool, #2672): the tool to give back on release.
@@ -675,16 +678,16 @@ impl Default for UiState {
}
impl UiState {
/// Records the current tool as the last one used in its shortcut group.
pub fn remember_group_tool(&mut self) {
let tool = self.tool;
if !self.group_tools.contains(&tool) {
self.group_tools.retain(|t| t.key() != tool.key());
self.group_tools.push(tool);
/// Records `tool` as the last one used in its shortcut group and toolbar slot.
pub fn remember_group_tool(&mut self, tool: Tool) {
if self.group_tools.first() != Some(&tool) {
self.group_tools.retain(|t| *t != tool);
self.group_tools.insert(0, tool);
}
}
/// The last tool used in the shortcut group of `key`, if any.
#[cfg(test)]
pub fn group_tool(&self, key: char) -> Option<Tool> {
self.group_tools.iter().copied().find(|t| t.key() == key)
}
+47 -7
View File
@@ -11,6 +11,10 @@
//! `paint_mouse::sync_tool_brush`) is kept the same way in `Preferences::tool_brushes` (#6212),
//! with the tool that owns the session brush (that brush itself is `Preferences::last_brush`).
//! Reset Tool and Reset All Tools change both, so the next save forgets what they reset.
//!
//! The tools used last (`UiState::group_tools`) are kept the same way in
//! `Preferences::group_tools` (#3569), so a tool picked from a flyout stays on its toolbar slot
//! and its key after a restart. A name this version doesn't know is skipped, as with the options.
use photocraft_engine::BrushSettings;
use serde_json::{Map, Value, json};
@@ -43,7 +47,7 @@ pub(crate) fn ended_by_tool(app: &mut PhotocraftApp) {
/// Temporary canvas overrides do not change `ui.tool`. Spring-loaded letters and the pen's
/// eraser end do, so remember the tool they will return to instead. A different explicit choice
/// (or beginning a type edit) owns the tool and stands, just as in `spring_tools::tick`.
fn selected(app: &PhotocraftApp) -> Tool {
pub(crate) fn selected(app: &PhotocraftApp) -> Tool {
let tool = if app.ui.tool == Tool::Eraser { app.stylus.tool_before_eraser.unwrap_or(app.ui.tool) } else { app.ui.tool };
if let Some(s) = &app.ui.spring
&& tool == s.tool
@@ -63,8 +67,9 @@ fn changed(o: &ToolOptions) -> Value {
if diff.is_empty() { Value::Null } else { Value::Object(diff) }
}
/// Remember the selected tool and its options once the user lets go of the pointer. Run every
/// frame, so choices made through the control channel or a tool preset are remembered too.
/// Remember the selected tool, the tools used last and the options once the user lets go of the
/// pointer. Run every frame, so choices made through the control channel or a tool preset are
/// remembered too.
pub fn persist(app: &mut PhotocraftApp, ctx: &egui::Context) {
if ctx.input(|i| i.pointer.any_down()) {
return;
@@ -80,6 +85,7 @@ pub fn persist(app: &mut PhotocraftApp, ctx: &egui::Context) {
ctx.request_repaint();
}
}
persist_group_tools(app, ctx, tool);
persist_brushes(app);
if app.prefs_rt.tool_options.as_ref() == Some(&app.ui.tool_options) {
return;
@@ -91,6 +97,24 @@ pub fn persist(app: &mut PhotocraftApp, ctx: &egui::Context) {
}
}
/// Remember the tools used last when the list changes, whether or not the selected tool did: a
/// tool picked during a drag that ends on the tool it began with changes only the list.
fn persist_group_tools(app: &mut PhotocraftApp, ctx: &egui::Context, tool: Tool) {
// Noted here as well: no frame follows a tool picked right before quitting.
app.ui.remember_group_tool(tool);
if app.prefs_rt.group_tools.as_ref() == Some(&app.ui.group_tools) {
return;
}
app.prefs_rt.group_tools = Some(app.ui.group_tools.clone());
if let Ok(now) = serde_json::to_value(&app.ui.group_tools)
&& app.session.prefs().group_tools != now
{
app.session.prefs.edit(|p| p.group_tools = now);
// As for the selected tool: prefs_ui::tick ran before the panels.
ctx.request_repaint();
}
}
/// Remember the painting tools' own brushes when they change (a compare per frame otherwise:
/// tip bitmaps are shared, so equal brushes compare by pointer). A brush that can't be remembered
/// yet (a sampled tip too big to embed, before the preset store takes it) keeps what was saved.
@@ -174,19 +198,35 @@ pub(crate) fn save_on_exit(app: &mut PhotocraftApp) -> Result<(), String> {
crate::prefs_ui::save_preferences(app)
}
/// Restore the tools used last, keeping their order. Anything that isn't a tool is skipped with a
/// warning: the rest of the list still holds.
fn restore_group_tools(app: &mut PhotocraftApp) {
let saved = app.session.prefs().group_tools.clone();
if !saved.is_null() && !saved.is_array() {
log::warn!("Preferences: the remembered group tools are not a list; using the defaults");
}
for v in saved.as_array().into_iter().flatten().rev() {
match serde_json::from_value::<Tool>(v.clone()) {
Ok(tool) => app.ui.remember_group_tool(tool),
Err(_) => log::warn!("Preferences: ignoring the remembered group tool {v}"),
}
}
}
/// Restore the selected tool before the first frame and its options field by field.
pub fn restore(app: &mut PhotocraftApp) {
restore_group_tools(app);
let saved = app.session.prefs().selected_tool.clone();
if !saved.is_null() {
match serde_json::from_value::<Tool>(saved) {
Ok(tool) => {
app.ui.tool = tool;
app.ui.remember_group_tool();
}
Ok(tool) => app.ui.tool = tool,
Err(err) => log::warn!("Preferences: ignoring the remembered selected tool: {err}"),
}
}
// Here rather than on the first frame, so a launch alone has nothing to save.
app.ui.remember_group_tool(app.ui.tool);
app.prefs_rt.selected_tool = Some(app.ui.tool);
app.prefs_rt.group_tools = Some(app.ui.group_tools.clone());
let saved = app.session.prefs().tool_options.clone();
if let Some(saved) = saved.as_object()
&& let Ok(Value::Object(mut cur)) = serde_json::to_value(&app.ui.tool_options)
+1 -1
View File
@@ -215,7 +215,7 @@ Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| UI-217-1 | Tool options, last Export As / Save for Web / Fill settings and font recents persist | partial | [#217](https://github.com/storytold/photocraft/issues/217) | Tool options (Preferences.toolOptions, tool_memory.rs, #2814; clamped to the options-bar ranges on restore), each painting tool's own brush (Preferences.toolBrushes + lastBrush, #6212) and the selected tool persist; Reset Tool / Reset All Tools (tool_reset.rs) clear them. Fill, Liquify, Export As (export_dialog.rs COMMAND) and Save for Web (file_ui.rs WEB_COMMAND) remember their last settings in prefs.dialogs. Missing: font recents (type.recentFonts is in the prefs audit's unused list) |
| UI-217-1 | Tool options, last Export As / Save for Web / Fill settings and font recents persist | partial | [#217](https://github.com/storytold/photocraft/issues/217) | Tool options (Preferences.toolOptions, tool_memory.rs, #2814; clamped to the options-bar ranges on restore), each painting tool's own brush (Preferences.toolBrushes + lastBrush, #6212) and the selected tool persist; Reset Tool / Reset All Tools (tool_reset.rs) clear them. The last tool of each toolbar slot and tool key persists too (Preferences.groupTools, #3569). Fill, Liquify, Export As (export_dialog.rs COMMAND) and Save for Web (file_ui.rs WEB_COMMAND) remember their last settings in prefs.dialogs. Missing: font recents (type.recentFonts is in the prefs audit's unused list) |
| UI-217-2 | Window size and dock width remembered across restarts | done | [#217](https://github.com/storytold/photocraft/issues/217) | eframe persistence (ui.ron): persist_window for the window, egui memory for the dock panel width; window_and_panel_geometry_survive_a_restart asserts the round trip (apps/photocraft/src/main.rs; b07a2e5b, #587) |
| UI-217-3 | File > Open starts in the last-used folder | done | [#217](https://github.com/storytold/photocraft/issues/217) | file_dialog.rs last_used_dir: open dialogs ask with initial_dir (the most recent file's folder) and apps services.rs passes it to rfd set_directory; open_starts_in_the_last_used_folder + last_used_dir_takes_the_most_recent_parent |
| UI-217-4 | Preferences all take effect; recent files persist and honour recent_file_count (#204) | missing | [#217](https://github.com/storytold/photocraft/issues/217) | See the prefs audit and BUG-204-* |
+3 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -10,7 +10,7 @@ id = "UI-217-1"
target = "Tool options, last Export As / Save for Web / Fill settings and font recents persist"
status = "partial"
issue = 217
note = "Tool options (Preferences.toolOptions, tool_memory.rs, #2814; clamped to the options-bar ranges on restore), each painting tool's own brush (Preferences.toolBrushes + lastBrush, #6212) and the selected tool persist; Reset Tool / Reset All Tools (tool_reset.rs) clear them. Fill, Liquify, Export As (export_dialog.rs COMMAND) and Save for Web (file_ui.rs WEB_COMMAND) remember their last settings in prefs.dialogs. Missing: font recents (type.recentFonts is in the prefs audit's unused list)"
note = "Tool options (Preferences.toolOptions, tool_memory.rs, #2814; clamped to the options-bar ranges on restore), each painting tool's own brush (Preferences.toolBrushes + lastBrush, #6212) and the selected tool persist; Reset Tool / Reset All Tools (tool_reset.rs) clear them. The last tool of each toolbar slot and tool key persists too (Preferences.groupTools, #3569). Fill, Liquify, Export As (export_dialog.rs COMMAND) and Save for Web (file_ui.rs WEB_COMMAND) remember their last settings in prefs.dialogs. Missing: font recents (type.recentFonts is in the prefs audit's unused list)"
[[item]]
id = "UI-217-2"