mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-07 12:13:10 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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(¤t) {
|
||||
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() {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
+1
-1
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user