From 2d1a3369525740a216b2c25a7880f593385232c7 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Fri, 15 Nov 2024 19:06:21 +0200 Subject: [PATCH 01/14] add an initial feature outline using the `pattern` command --- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/pattern.rs | 10 +++- src-tauri/src/commands/stitches.rs | 22 ++++++++ src-tauri/src/core/commands/mod.rs | 12 ++++ src-tauri/src/core/commands/stitches.rs | 56 +++++++++++++++++++ src-tauri/src/core/mod.rs | 1 + .../src/core/pattern/stitches/stitches.rs | 12 ++-- src-tauri/src/main.rs | 3 +- src-tauri/src/state.rs | 11 +--- src-tauri/tests/utils/mod.rs | 2 +- src/api/stitches.ts | 6 ++ src/components/CanvasPanel.vue | 37 ++++++------ src/services/events/pattern.ts | 10 ---- src/types/events/pattern.ts | 20 ------- 14 files changed, 140 insertions(+), 63 deletions(-) create mode 100644 src-tauri/src/commands/stitches.rs create mode 100644 src-tauri/src/core/commands/mod.rs create mode 100644 src-tauri/src/core/commands/stitches.rs create mode 100644 src/api/stitches.ts delete mode 100644 src/services/events/pattern.ts delete mode 100644 src/types/events/pattern.ts diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index fc0b067..76f76b6 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod path; pub mod pattern; +pub mod stitches; diff --git a/src-tauri/src/commands/pattern.rs b/src-tauri/src/commands/pattern.rs index 2145ab6..ab74516 100644 --- a/src-tauri/src/commands/pattern.rs +++ b/src-tauri/src/commands/pattern.rs @@ -34,7 +34,8 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State( // It is safe to unwrap here, because the pattern is always serializable. let result = (pattern_key.clone(), borsh::to_vec(&patproj).unwrap()); - state.patterns.insert(pattern_key, patproj); + state.patterns.insert(pattern_key.clone(), patproj); + state.history.insert(pattern_key, Vec::new()); log::trace!("Pattern has been created"); Ok(result) @@ -88,7 +90,9 @@ pub fn save_pattern( #[tauri::command] pub fn close_pattern(pattern_key: PatternKey, state: tauri::State) { log::trace!("Closing pattern {:?}", pattern_key); - state.write().unwrap().patterns.remove(&pattern_key); + let mut state = state.write().unwrap(); + state.patterns.remove(&pattern_key); + state.history.remove(&pattern_key); log::trace!("Pattern closed"); } diff --git a/src-tauri/src/commands/stitches.rs b/src-tauri/src/commands/stitches.rs new file mode 100644 index 0000000..ee81b47 --- /dev/null +++ b/src-tauri/src/commands/stitches.rs @@ -0,0 +1,22 @@ +use crate::{ + core::{ + commands::{AddStitchCommand, Command}, + pattern::Stitch, + }, + error::CommandResult, + state::{AppStateType, PatternKey}, +}; + +#[tauri::command] +pub fn add_stitch( + pattern_key: PatternKey, + stitch: Stitch, + window: tauri::WebviewWindow, + state: tauri::State, +) -> CommandResult<()> { + let mut state = state.write().unwrap(); + let command = AddStitchCommand::new(stitch); + command.execute(&window, state.patterns.get_mut(&pattern_key).unwrap())?; + state.history.get_mut(&pattern_key).unwrap().push(Box::new(command)); + Ok(()) +} diff --git a/src-tauri/src/core/commands/mod.rs b/src-tauri/src/core/commands/mod.rs new file mode 100644 index 0000000..71fc586 --- /dev/null +++ b/src-tauri/src/core/commands/mod.rs @@ -0,0 +1,12 @@ +use anyhow::Result; +use tauri::WebviewWindow; + +use super::pattern::PatternProject; + +mod stitches; +pub use stitches::*; + +pub trait Command: Send + Sync { + fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; + fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; +} diff --git a/src-tauri/src/core/commands/stitches.rs b/src-tauri/src/core/commands/stitches.rs new file mode 100644 index 0000000..19ad9b3 --- /dev/null +++ b/src-tauri/src/core/commands/stitches.rs @@ -0,0 +1,56 @@ +use std::sync::OnceLock; + +use anyhow::Result; +use tauri::{Emitter, WebviewWindow}; + +use crate::core::pattern::{PatternProject, Stitch, StitchConflicts}; + +use super::Command; + +pub struct AddStitchCommand { + stitch: Stitch, + conflicts: OnceLock, +} + +impl AddStitchCommand { + pub fn new(stitch: Stitch) -> Self { + Self { + stitch, + conflicts: OnceLock::new(), + } + } +} + +impl Command for AddStitchCommand { + fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { + let conflicts = patproj.pattern.add_stitch(self.stitch.clone()); + self.conflicts.set(conflicts.clone()).unwrap(); + window.emit("stitches:remove_many", conflicts)?; + Ok(()) + } + + fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { + let conflicts = self.conflicts.get().unwrap(); + + debug_assert!(patproj.pattern.remove_stitch(self.stitch.clone())); + + for stitch in conflicts.fullstitches.iter() { + debug_assert!(patproj.pattern.add_stitch(Stitch::Full(stitch.clone())).is_empty()); + } + + for stitch in conflicts.partstitches.iter() { + debug_assert!(patproj.pattern.add_stitch(Stitch::Part(stitch.clone())).is_empty()); + } + + if let Some(node) = &conflicts.node { + debug_assert!(patproj.pattern.add_stitch(Stitch::Node(node.clone())).is_empty()); + } + + if let Some(line) = &conflicts.line { + debug_assert!(patproj.pattern.add_stitch(Stitch::Line(line.clone())).is_empty()); + } + + window.emit("stitches:add_many", conflicts.clone())?; + Ok(()) + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index c67aa6d..597dbab 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,2 +1,3 @@ +pub mod commands; pub mod parser; pub mod pattern; diff --git a/src-tauri/src/core/pattern/stitches/stitches.rs b/src-tauri/src/core/pattern/stitches/stitches.rs index ab9e122..00d4376 100644 --- a/src-tauri/src/core/pattern/stitches/stitches.rs +++ b/src-tauri/src/core/pattern/stitches/stitches.rs @@ -23,10 +23,10 @@ pub enum Stitch { #[derive(Debug, Default, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct StitchConflicts { - fullstitches: Vec, - partstitches: Vec, - node: Option, - line: Option, + pub fullstitches: Vec, + pub partstitches: Vec, + pub node: Option, + pub line: Option, } impl StitchConflicts { @@ -63,6 +63,10 @@ impl StitchConflicts { self.line = line; self } + + pub fn is_empty(&self) -> bool { + self.fullstitches.is_empty() && self.partstitches.is_empty() && self.node.is_none() && self.line.is_none() + } } #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c45e347..9a2a73e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -28,7 +28,7 @@ fn main() { Ok(()) }) - .manage(RwLock::new(state::AppState::new())) + .manage(RwLock::new(state::AppState::default())) .plugin(logger::setup_logger().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) @@ -40,6 +40,7 @@ fn main() { commands::pattern::close_pattern, commands::pattern::get_pattern_file_path, commands::pattern::add_palette_item, + commands::stitches::add_stitch, ]) .run(tauri::generate_context!()) .expect("Error while running Embroidery Studio"); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 9fd7d1a..4f538eb 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use crate::core::pattern::PatternProject; +use crate::core::{commands::Command, pattern::PatternProject}; #[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[repr(transparent)] @@ -15,15 +15,10 @@ impl From for PatternKey { } } +#[derive(Default)] pub struct AppState { pub patterns: HashMap, -} - -impl AppState { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Self { patterns: HashMap::new() } - } + pub history: HashMap>>, } pub type AppStateType = std::sync::RwLock; diff --git a/src-tauri/tests/utils/mod.rs b/src-tauri/tests/utils/mod.rs index 499df9e..9cee422 100644 --- a/src-tauri/tests/utils/mod.rs +++ b/src-tauri/tests/utils/mod.rs @@ -8,7 +8,7 @@ use embroidery_studio::state::AppState; pub fn setup_app() -> App { mock_builder() - .manage(std::sync::RwLock::new(AppState::new())) + .manage(std::sync::RwLock::new(AppState::default())) .build(generate_context!()) .unwrap() } diff --git a/src/api/stitches.ts b/src/api/stitches.ts new file mode 100644 index 0000000..e052cc9 --- /dev/null +++ b/src/api/stitches.ts @@ -0,0 +1,6 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { FullStitch, Line, Node, PartStitch } from "#/types/pattern/pattern"; + +type Stitch = { full: FullStitch } | { part: PartStitch } | { node: Node } | { line: Line }; + +export const addStitch = (patternKey: string, stitch: Stitch) => invoke("add_stitch", { patternKey, stitch }); diff --git a/src/components/CanvasPanel.vue b/src/components/CanvasPanel.vue index 17f4202..e1b06e7 100644 --- a/src/components/CanvasPanel.vue +++ b/src/components/CanvasPanel.vue @@ -7,9 +7,8 @@ import { onMounted, onUnmounted, ref, watch } from "vue"; import { CanvasService } from "#/services/canvas"; import { useAppStateStore } from "#/stores/state"; - import { emitStitchCreated, emitStitchRemoved } from "#/services/events/pattern"; + import * as stitchesApi from "#/api/stitches"; import { PartStitchDirection, StitchKind } from "#/types/pattern/pattern"; - import type { RemovedStitchPayload, StitchEventPayload } from "#/types/events/pattern"; import type { FullStitch, Line, Node, PartStitch } from "#/types/pattern/pattern"; import type { PatternProject } from "#/types/pattern/project"; @@ -69,7 +68,7 @@ palindex, kind, }; - await emitStitchCreated(patternKey, { full: fullstitch }); + await stitchesApi.addStitch(patternKey, { full: fullstitch }); canvasService.drawFullStitch(fullstitch, palitem.color); break; } @@ -87,7 +86,7 @@ kind, direction, }; - await emitStitchCreated(patternKey, { part: partstitch }); + await stitchesApi.addStitch(patternKey, { part: partstitch }); canvasService.drawPartStitch(partstitch, palitem.color); break; } @@ -103,7 +102,7 @@ palindex, kind, }; - await emitStitchCreated(patternKey, { line }); + await stitchesApi.addStitch(patternKey, { line }); canvasService.drawLine(line, palitem.color); break; } @@ -117,7 +116,7 @@ kind, rotated: modifier, }; - await emitStitchCreated(patternKey, { node }); + await stitchesApi.addStitch(patternKey, { node }); canvasService.drawNode(node, palitem.color); break; } @@ -139,7 +138,7 @@ const ydp = point.y - y; // The current pattern is always available here. - const patternKey = appStateStore.state.currentPattern!.key; + // const patternKey = appStateStore.state.currentPattern!.key; const palindex = appStateStore.state.selectedPaletteItemIndex; const tool = appStateStore.state.selectedStitchTool; @@ -153,7 +152,7 @@ palindex, kind, }; - await emitStitchRemoved(patternKey, { full: fullstitch }); + // await emitStitchRemoved(patternKey, { full: fullstitch }); canvasService.removeFullStitch(fullstitch); break; } @@ -171,7 +170,7 @@ kind, direction, }; - await emitStitchRemoved(patternKey, { part: partstitch }); + // await emitStitchRemoved(patternKey, { part: partstitch }); canvasService.removePartStitch(partstitch); break; } @@ -185,7 +184,7 @@ kind, rotated: false, }; - await emitStitchRemoved(patternKey, { node }); + // await emitStitchRemoved(patternKey, { node }); canvasService.removeNode(node); break; } @@ -211,13 +210,19 @@ } } + export interface StitchesRemoveManyPayload { + fullstitches: FullStitch[]; + partstitches: PartStitch[]; + line?: Line; + node?: Node; + } + const appWindow = getCurrentWindow(); - const unlistenRemoveStitches = await appWindow.listen>( - "pattern:stitches:remove", - (e) => { - const { payload } = e.payload; - if (payload.fullstitches) canvasService.removeFullStitches(payload.fullstitches); - if (payload.partstitches) canvasService.removePartStitches(payload.partstitches); + const unlistenRemoveStitches = await appWindow.listen( + "stitches:remove_many", + ({ payload }) => { + canvasService.removeFullStitches(payload.fullstitches); + canvasService.removePartStitches(payload.partstitches); if (payload.line) canvasService.removeLine(payload.line); if (payload.node) canvasService.removeNode(payload.node); }, diff --git a/src/services/events/pattern.ts b/src/services/events/pattern.ts deleted file mode 100644 index 2c1bc66..0000000 --- a/src/services/events/pattern.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { emit } from "@tauri-apps/api/event"; -import { type CreatedStitchPayload } from "#/types/events/pattern"; - -export function emitStitchCreated(patternKey: string, payload: CreatedStitchPayload) { - return emit("pattern:stitch:create", { patternKey, payload }); -} - -export function emitStitchRemoved(patternKey: string, payload: CreatedStitchPayload) { - return emit("pattern:stitch:remove", { patternKey, payload }); -} diff --git a/src/types/events/pattern.ts b/src/types/events/pattern.ts deleted file mode 100644 index 51cfd4d..0000000 --- a/src/types/events/pattern.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { FullStitch, Line, Node, PartStitch } from "../pattern/pattern"; - -export interface StitchEventPayload { - patternKey: string; - payload: T; -} - -export interface CreatedStitchPayload { - full?: FullStitch; - part?: PartStitch; - node?: Node; - line?: Line; -} - -export interface RemovedStitchPayload { - fullstitches?: FullStitch[]; - partstitches?: PartStitch[]; - line?: Line; - node?: Node; -} From 698cb8edab0ea94312d6991586833a22d67a62b8 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Sat, 16 Nov 2024 10:20:31 +0200 Subject: [PATCH 02/14] add shortcuts system --- src/App.vue | 180 ++---------------- src/components/toolbar/DropdownTieredMenu.vue | 32 ---- src/components/toolbar/MainMenu.vue | 130 +++++++++++++ src/main.ts | 6 +- src/stores/patproj.ts | 62 ++++++ src/stores/state.ts | 56 +++--- 6 files changed, 236 insertions(+), 230 deletions(-) delete mode 100644 src/components/toolbar/DropdownTieredMenu.vue create mode 100644 src/components/toolbar/MainMenu.vue create mode 100644 src/stores/patproj.ts diff --git a/src/App.vue b/src/App.vue index e5e0210..57d7804 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,7 @@
@@ -12,7 +12,7 @@ - + @@ -55,185 +55,27 @@ diff --git a/src/components/toolbar/DropdownTieredMenu.vue b/src/components/toolbar/DropdownTieredMenu.vue deleted file mode 100644 index 794f4be..0000000 --- a/src/components/toolbar/DropdownTieredMenu.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/src/components/toolbar/MainMenu.vue b/src/components/toolbar/MainMenu.vue new file mode 100644 index 0000000..901014d --- /dev/null +++ b/src/components/toolbar/MainMenu.vue @@ -0,0 +1,130 @@ + + + diff --git a/src/main.ts b/src/main.ts index 6655be5..036854c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,9 @@ import { createApp } from "vue"; +import { createPinia } from "pinia"; +import piniaPluginPersistedState from "pinia-plugin-persistedstate"; import { PrimeVue } from "@primevue/core"; import { Tooltip, ConfirmationService } from "primevue"; import Aura from "@primevue/themes/aura"; -import { createPinia } from "pinia"; -import { createPersistedState } from "pinia-plugin-persistedstate"; import "primeicons/primeicons.css"; import "./assets/styles.css"; @@ -11,7 +11,7 @@ import "./assets/styles.css"; import App from "./App.vue"; const pinia = createPinia(); -pinia.use(createPersistedState({ storage: sessionStorage, auto: true })); +pinia.use(piniaPluginPersistedState); const app = createApp(App); app.use(pinia); diff --git a/src/stores/patproj.ts b/src/stores/patproj.ts new file mode 100644 index 0000000..75a1b73 --- /dev/null +++ b/src/stores/patproj.ts @@ -0,0 +1,62 @@ +import { ref } from "vue"; +import { defineStore } from "pinia"; +import { useConfirm } from "primevue"; +import { useAppStateStore } from "./state"; +import * as patternApi from "#/api/pattern"; +import type { PatternProject } from "#/types/pattern/project"; +import type { PaletteItem } from "#/types/pattern/pattern"; + +export const usePatternProjectStore = defineStore("pattern-project", () => { + const confirm = useConfirm(); + const appStateStore = useAppStateStore(); + + const loading = ref(false); + const patproj = ref(); + + async function addPaletteItem(pi: PaletteItem) { + if (!patproj.value || !appStateStore.state.currentPattern) return; + await patternApi.addPaletteItem(appStateStore.state.currentPattern.key, pi); + patproj.value.pattern.palette.push(pi); + } + + async function handleCommand(command: () => Promise) { + try { + loading.value = true; + await command(); + } catch (err) { + confirm.require({ + header: "Error", + message: err as string, + icon: "pi pi-info-circle", + acceptLabel: "OK", + acceptProps: { outlined: true }, + rejectLabel: "Cancel", + rejectProps: { severity: "secondary", outlined: true }, + }); + } finally { + loading.value = false; + } + } + + const loadPattern = (path: string) => + handleCommand(async () => { + patproj.value = await patternApi.loadPattern(path); + appStateStore.addOpenedPattern(patproj.value.pattern.info.title, path); + }); + const createPattern = () => + handleCommand(async () => { + const { key, pattern } = await patternApi.createPattern(); + patproj.value = pattern; + appStateStore.addOpenedPattern(patproj.value.pattern.info.title, key); + }); + const savePattern = (key: string, path: string) => handleCommand(() => patternApi.savePattern(key, path)); + const closePattern = (key: string) => + handleCommand(async () => { + await patternApi.closePattern(key); + appStateStore.removeCurrentPattern(); + if (!appStateStore.state.currentPattern) patproj.value = undefined; + else await loadPattern(appStateStore.state.currentPattern.key); + }); + + return { loading, patproj, addPaletteItem, loadPattern, createPattern, savePattern, closePattern }; +}); diff --git a/src/stores/state.ts b/src/stores/state.ts index 31e26e2..b041a63 100644 --- a/src/stores/state.ts +++ b/src/stores/state.ts @@ -14,32 +14,36 @@ export interface AppState { currentPattern?: OpenedPattern; } -export const useAppStateStore = defineStore("embroidery-studio-state", () => { - const state = reactive({ - selectedStitchTool: StitchKind.Full, - }); +export const useAppStateStore = defineStore( + "embroidery-studio-state", + () => { + const state = reactive({ + selectedStitchTool: StitchKind.Full, + }); - /** - * Adds the opened pattern to the app state. - * If the pattern is already opened, it will not be added again. - * - * @param title The title of the pattern. - * @param key The key of the pattern. Actually, the key is the file path of the pattern. - */ - function addOpenedPattern(title: string, key: string) { - if (!state.openedPatterns) state.openedPatterns = []; - const openedPattern: OpenedPattern = { title, key }; - if (state.openedPatterns.findIndex((p) => p.key === key) < 0) state.openedPatterns.push(openedPattern); - state.currentPattern = openedPattern; - } + /** + * Adds the opened pattern to the app state. + * If the pattern is already opened, it will not be added again. + * + * @param title The title of the pattern. + * @param key The key of the pattern. Actually, the key is the file path of the pattern. + */ + function addOpenedPattern(title: string, key: string) { + if (!state.openedPatterns) state.openedPatterns = []; + const openedPattern: OpenedPattern = { title, key }; + if (state.openedPatterns.findIndex((p) => p.key === key) < 0) state.openedPatterns.push(openedPattern); + state.currentPattern = openedPattern; + } - function removeCurrentPattern() { - if (!state.openedPatterns || !state.currentPattern) return; - const index = state.openedPatterns.findIndex((p) => p.key === state.currentPattern!.key); - if (index >= 0) state.openedPatterns.splice(index, 1); - if (state.openedPatterns.length) state.currentPattern = state.openedPatterns[0]; - else state.currentPattern = undefined; - } + function removeCurrentPattern() { + if (!state.openedPatterns || !state.currentPattern) return; + const index = state.openedPatterns.findIndex((p) => p.key === state.currentPattern!.key); + if (index >= 0) state.openedPatterns.splice(index, 1); + if (state.openedPatterns.length) state.currentPattern = state.openedPatterns[0]; + else state.currentPattern = undefined; + } - return { state, addOpenedPattern, removeCurrentPattern }; -}); + return { state, addOpenedPattern, removeCurrentPattern }; + }, + { persist: { storage: sessionStorage } }, +); From 29924b6e6f9fda85847332dd6854d5c7b1459241 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Sat, 16 Nov 2024 10:41:34 +0200 Subject: [PATCH 03/14] move palette command to a separate module --- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/palette.rs | 11 +++++++++++ src-tauri/src/commands/pattern.rs | 9 +-------- src-tauri/src/main.rs | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 src-tauri/src/commands/palette.rs diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 76f76b6..76f1d3a 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod palette; pub mod path; pub mod pattern; pub mod stitches; diff --git a/src-tauri/src/commands/palette.rs b/src-tauri/src/commands/palette.rs new file mode 100644 index 0000000..979b1bd --- /dev/null +++ b/src-tauri/src/commands/palette.rs @@ -0,0 +1,11 @@ +use crate::{ + core::pattern::PaletteItem, + state::{AppStateType, PatternKey}, +}; + +#[tauri::command] +pub fn add_palette_item(pattern_key: PatternKey, palette_item: PaletteItem, state: tauri::State) { + let mut state = state.write().unwrap(); + let patproj = state.patterns.get_mut(&pattern_key).unwrap(); + patproj.pattern.palette.push(palette_item); +} diff --git a/src-tauri/src/commands/pattern.rs b/src-tauri/src/commands/pattern.rs index ab74516..78d2c37 100644 --- a/src-tauri/src/commands/pattern.rs +++ b/src-tauri/src/commands/pattern.rs @@ -1,7 +1,7 @@ use crate::{ core::{ parser::{self, PatternFormat}, - pattern::{display::DisplaySettings, print::PrintSettings, PaletteItem, Pattern, PatternProject}, + pattern::{display::DisplaySettings, print::PrintSettings, Pattern, PatternProject}, }, error::CommandResult, state::{AppStateType, PatternKey}, @@ -102,10 +102,3 @@ pub fn get_pattern_file_path(pattern_key: PatternKey, state: tauri::State) { - let mut state = state.write().unwrap(); - let patproj = state.patterns.get_mut(&pattern_key).unwrap(); - patproj.pattern.palette.push(palette_item); -} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9a2a73e..1977081 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -39,7 +39,7 @@ fn main() { commands::pattern::save_pattern, commands::pattern::close_pattern, commands::pattern::get_pattern_file_path, - commands::pattern::add_palette_item, + commands::palette::add_palette_item, commands::stitches::add_stitch, ]) .run(tauri::generate_context!()) From c8f783dfebcbb2fe4527a9a3369d4d6aee5765ea Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Sun, 17 Nov 2024 09:24:10 +0200 Subject: [PATCH 04/14] add a basic history implementation for the stitch creation --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/commands/history.rs | 26 ++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/pattern.rs | 19 ++--- src-tauri/src/core/commands/mod.rs | 14 +++- src-tauri/src/core/commands/stitches.rs | 32 +++----- src-tauri/src/core/pattern/pattern.rs | 12 +-- .../src/core/pattern/stitches/fullstitch.rs | 2 +- src-tauri/src/core/pattern/stitches/line.rs | 2 +- src-tauri/src/core/pattern/stitches/node.rs | 2 +- .../src/core/pattern/stitches/partstitch.rs | 2 +- .../src/core/pattern/stitches/special.rs | 2 +- .../src/core/pattern/stitches/stitches.rs | 79 ++++++++++++++----- src-tauri/src/main.rs | 2 + src-tauri/src/state.rs | 50 +++++++++++- src/api/history.ts | 4 + src/api/stitches.ts | 4 +- src/components/CanvasPanel.vue | 61 ++++++++++---- 19 files changed, 236 insertions(+), 80 deletions(-) create mode 100644 src-tauri/src/commands/history.rs create mode 100644 src/api/history.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4e29f88..150a3b3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1004,6 +1004,7 @@ dependencies = [ "anyhow", "borsh", "byteorder", + "dyn-clone", "encoding_rs", "hex", "log", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8162ac5..097d10e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,3 +41,4 @@ log = "0.4.22" # Other ordered-float = { version = "4.5.0", features = ["borsh", "serde"] } +dyn-clone = "1.0.17" diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs new file mode 100644 index 0000000..3f841c3 --- /dev/null +++ b/src-tauri/src/commands/history.rs @@ -0,0 +1,26 @@ +use tauri::WebviewWindow; + +use crate::{ + error::CommandResult, + state::{AppStateType, PatternKey}, +}; + +#[tauri::command] +pub fn undo(window: WebviewWindow, state: tauri::State, pattern_key: PatternKey) -> CommandResult<()> { + let mut state = state.write().unwrap(); + let history = state.history.get_mut(&pattern_key).unwrap(); + if let Some(command) = history.undo() { + command.revoke(&window, state.patterns.get_mut(&pattern_key).unwrap())?; + } + Ok(()) +} + +#[tauri::command] +pub fn redo(window: WebviewWindow, state: tauri::State, pattern_key: PatternKey) -> CommandResult<()> { + let mut state = state.write().unwrap(); + let history = state.history.get_mut(&pattern_key).unwrap(); + if let Some(command) = history.redo() { + command.execute(&window, state.patterns.get_mut(&pattern_key).unwrap())?; + } + Ok(()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 76f1d3a..d377086 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod history; pub mod palette; pub mod path; pub mod pattern; diff --git a/src-tauri/src/commands/pattern.rs b/src-tauri/src/commands/pattern.rs index 78d2c37..16e170a 100644 --- a/src-tauri/src/commands/pattern.rs +++ b/src-tauri/src/commands/pattern.rs @@ -13,7 +13,7 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State { log::trace!("Pattern has been already loaded"); pattern.to_owned() @@ -22,20 +22,19 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State parser::xsd::parse_pattern(file_path)?, PatternFormat::Oxs => parser::oxs::parse_pattern(file_path)?, PatternFormat::EmbProj => parser::embproj::parse_pattern(file_path)?, }; - pattern.file_path = new_file_path; + patproj.file_path = new_file_path; - pattern + patproj } }; - let result = borsh::to_vec(&pattern)?; + let result = borsh::to_vec(&patproj)?; - state.patterns.insert(pattern_key.clone(), pattern.clone()); - state.history.insert(pattern_key, Vec::new()); + state.insert_pattern(pattern_key, patproj); log::trace!("Pattern loaded"); Ok(result) @@ -61,8 +60,7 @@ pub fn create_pattern( // It is safe to unwrap here, because the pattern is always serializable. let result = (pattern_key.clone(), borsh::to_vec(&patproj).unwrap()); - state.patterns.insert(pattern_key.clone(), patproj); - state.history.insert(pattern_key, Vec::new()); + state.insert_pattern(pattern_key, patproj); log::trace!("Pattern has been created"); Ok(result) @@ -91,8 +89,7 @@ pub fn save_pattern( pub fn close_pattern(pattern_key: PatternKey, state: tauri::State) { log::trace!("Closing pattern {:?}", pattern_key); let mut state = state.write().unwrap(); - state.patterns.remove(&pattern_key); - state.history.remove(&pattern_key); + state.remove_pattern(&pattern_key); log::trace!("Pattern closed"); } diff --git a/src-tauri/src/core/commands/mod.rs b/src-tauri/src/core/commands/mod.rs index 71fc586..6f4126a 100644 --- a/src-tauri/src/core/commands/mod.rs +++ b/src-tauri/src/core/commands/mod.rs @@ -6,7 +6,19 @@ use super::pattern::PatternProject; mod stitches; pub use stitches::*; -pub trait Command: Send + Sync { +/// A command that can be executed and revoked. +pub trait Command: Send + Sync + dyn_clone::DynClone { + /// Execute the command. + /// + /// The `window` parameter is the webview window that the command should use to emit events. + /// The `patproj` parameter is the pattern project that the command should modify. fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; + + /// Revoke the command. + /// + /// The `window` parameter is the webview window that the command should use to emit events. + /// The `patproj` parameter is the pattern project that the command should modify. fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; } + +dyn_clone::clone_trait_object!(Command); diff --git a/src-tauri/src/core/commands/stitches.rs b/src-tauri/src/core/commands/stitches.rs index 19ad9b3..f1b3fa4 100644 --- a/src-tauri/src/core/commands/stitches.rs +++ b/src-tauri/src/core/commands/stitches.rs @@ -7,6 +7,7 @@ use crate::core::pattern::{PatternProject, Stitch, StitchConflicts}; use super::Command; +#[derive(Clone)] pub struct AddStitchCommand { stitch: Stitch, conflicts: OnceLock, @@ -23,34 +24,23 @@ impl AddStitchCommand { impl Command for AddStitchCommand { fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { - let conflicts = patproj.pattern.add_stitch(self.stitch.clone()); - self.conflicts.set(conflicts.clone()).unwrap(); + let conflicts = patproj.pattern.add_stitch(self.stitch); + if self.conflicts.get().is_none() { + self.conflicts.set(conflicts.clone()).unwrap(); + } + window.emit("stitches:add_one", self.stitch)?; window.emit("stitches:remove_many", conflicts)?; Ok(()) } fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { let conflicts = self.conflicts.get().unwrap(); - - debug_assert!(patproj.pattern.remove_stitch(self.stitch.clone())); - - for stitch in conflicts.fullstitches.iter() { - debug_assert!(patproj.pattern.add_stitch(Stitch::Full(stitch.clone())).is_empty()); - } - - for stitch in conflicts.partstitches.iter() { - debug_assert!(patproj.pattern.add_stitch(Stitch::Part(stitch.clone())).is_empty()); + patproj.pattern.remove_stitch(self.stitch); + for stitch in conflicts.chain() { + patproj.pattern.add_stitch(stitch); } - - if let Some(node) = &conflicts.node { - debug_assert!(patproj.pattern.add_stitch(Stitch::Node(node.clone())).is_empty()); - } - - if let Some(line) = &conflicts.line { - debug_assert!(patproj.pattern.add_stitch(Stitch::Line(line.clone())).is_empty()); - } - - window.emit("stitches:add_many", conflicts.clone())?; + window.emit("stitches:remove_one", self.stitch)?; + window.emit("stitches:add_many", conflicts)?; Ok(()) } } diff --git a/src-tauri/src/core/pattern/pattern.rs b/src-tauri/src/core/pattern/pattern.rs index 7ca4e84..53ee59b 100644 --- a/src-tauri/src/core/pattern/pattern.rs +++ b/src-tauri/src/core/pattern/pattern.rs @@ -18,6 +18,7 @@ pub struct Pattern { } impl Pattern { + /// Adds a stitch to the pattern and returns any conflicts that may have arisen. pub fn add_stitch(&mut self, stitch: Stitch) -> StitchConflicts { log::trace!("Adding stitch"); match stitch { @@ -48,13 +49,14 @@ impl Pattern { } } - pub fn remove_stitch(&mut self, stitch: Stitch) -> bool { + /// Removes and returns a stitch from the pattern. + pub fn remove_stitch(&mut self, stitch: Stitch) -> Option { log::trace!("Removing stitch"); match stitch { - Stitch::Full(fullstitch) => self.fullstitches.remove(&fullstitch), - Stitch::Part(partstitch) => self.partstitches.remove(&partstitch), - Stitch::Node(node) => self.nodes.remove(&node), - Stitch::Line(line) => self.lines.remove(&line), + Stitch::Full(fullstitch) => self.fullstitches.remove(&fullstitch).map(|fs| fs.into()), + Stitch::Part(partstitch) => self.partstitches.remove(&partstitch).map(|ps| ps.into()), + Stitch::Node(node) => self.nodes.remove(&node).map(|node| node.into()), + Stitch::Line(line) => self.lines.remove(&line).map(|line| line.into()), } } } diff --git a/src-tauri/src/core/pattern/stitches/fullstitch.rs b/src-tauri/src/core/pattern/stitches/fullstitch.rs index 4b2a5de..db8b25b 100644 --- a/src-tauri/src/core/pattern/stitches/fullstitch.rs +++ b/src-tauri/src/core/pattern/stitches/fullstitch.rs @@ -5,7 +5,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use super::partstitch::*; use crate::core::pattern::Coord; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct FullStitch { pub x: Coord, pub y: Coord, diff --git a/src-tauri/src/core/pattern/stitches/line.rs b/src-tauri/src/core/pattern/stitches/line.rs index a9d0eb6..9dfa21c 100644 --- a/src-tauri/src/core/pattern/stitches/line.rs +++ b/src-tauri/src/core/pattern/stitches/line.rs @@ -4,7 +4,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::core::pattern::Coord; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct Line { pub x: (Coord, Coord), pub y: (Coord, Coord), diff --git a/src-tauri/src/core/pattern/stitches/node.rs b/src-tauri/src/core/pattern/stitches/node.rs index 954fd03..a44558e 100644 --- a/src-tauri/src/core/pattern/stitches/node.rs +++ b/src-tauri/src/core/pattern/stitches/node.rs @@ -4,7 +4,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::core::pattern::Coord; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct Node { pub x: Coord, pub y: Coord, diff --git a/src-tauri/src/core/pattern/stitches/partstitch.rs b/src-tauri/src/core/pattern/stitches/partstitch.rs index fa070b4..cdd240e 100644 --- a/src-tauri/src/core/pattern/stitches/partstitch.rs +++ b/src-tauri/src/core/pattern/stitches/partstitch.rs @@ -5,7 +5,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use super::fullstitch::*; use crate::core::pattern::Coord; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct PartStitch { pub x: Coord, pub y: Coord, diff --git a/src-tauri/src/core/pattern/stitches/special.rs b/src-tauri/src/core/pattern/stitches/special.rs index a6504ca..732174e 100644 --- a/src-tauri/src/core/pattern/stitches/special.rs +++ b/src-tauri/src/core/pattern/stitches/special.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use super::{Line, Node}; use crate::core::pattern::Coord; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct SpecialStitch { pub x: Coord, pub y: Coord, diff --git a/src-tauri/src/core/pattern/stitches/stitches.rs b/src-tauri/src/core/pattern/stitches/stitches.rs index 00d4376..93c89c4 100644 --- a/src-tauri/src/core/pattern/stitches/stitches.rs +++ b/src-tauri/src/core/pattern/stitches/stitches.rs @@ -12,7 +12,7 @@ mod tests; pub type Coord = ordered_float::NotNan; -#[derive(Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "lowercase")] pub enum Stitch { Full(FullStitch), @@ -21,6 +21,30 @@ pub enum Stitch { Line(Line), } +impl From for Stitch { + fn from(fullstitch: FullStitch) -> Self { + Self::Full(fullstitch) + } +} + +impl From for Stitch { + fn from(partstitch: PartStitch) -> Self { + Self::Part(partstitch) + } +} + +impl From for Stitch { + fn from(node: Node) -> Self { + Self::Node(node) + } +} + +impl From for Stitch { + fn from(line: Line) -> Self { + Self::Line(line) + } +} + #[derive(Debug, Default, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct StitchConflicts { pub fullstitches: Vec, @@ -64,9 +88,22 @@ impl StitchConflicts { self } + /// Returns `true` if there are no conflicts. pub fn is_empty(&self) -> bool { self.fullstitches.is_empty() && self.partstitches.is_empty() && self.node.is_none() && self.line.is_none() } + + /// Returns an iterator over all the stitches. + pub fn chain<'a>(&'a self) -> impl Iterator + 'a { + self + .fullstitches + .iter() + .cloned() + .map(Stitch::Full) + .chain(self.partstitches.iter().cloned().map(Stitch::Part)) + .chain(self.node.iter().cloned().map(Stitch::Node)) + .chain(self.line.iter().cloned().map(Stitch::Line)) + } } #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] @@ -84,21 +121,25 @@ impl Stitches { self.inner.iter() } - pub fn retain bool>(&mut self, f: F) { - self.inner.retain(f) - } - #[cfg(test)] pub fn len(&self) -> usize { self.inner.len() } + /// Inserts a stitch into the set, replacing the existing one. + /// Returns the replaced stitch if any. pub fn insert(&mut self, stitch: T) -> Option { + // We need to use the `replace` method to get the replaced value from the set. + // We need to return the previous value to pass it back to the caller, so it can be used to update the pattern on the frontend. self.inner.replace(stitch) } - pub fn remove(&mut self, stitch: &T) -> bool { - self.inner.remove(stitch) + /// Removes and returns a stitch from the set. + pub fn remove(&mut self, stitch: &T) -> Option { + // We need to use the `take` method to get the actual value from the set. + // The passed `stitch` contains only the fields that are used for ordering (coordinates, kind, etc.). + // Hovewer, we need to return the actual stitch that contains all the other values (mainly, palindex), so it can be used to update the pattern on the frontend. + self.inner.take(stitch) } pub fn get(&self, stitch: &T) -> Option<&T> { @@ -139,7 +180,7 @@ impl Stitches { FullStitch { y, kind, ..*fullstitch }, FullStitch { x, y, kind, ..*fullstitch }, ] { - self.remove(&petite).then(|| conflicts.push(petite)); + self.remove(&petite).inspect(|&petite| conflicts.push(petite)); } conflicts @@ -158,7 +199,7 @@ impl Stitches { kind: FullStitchKind::Full, }; - self.remove(&fullstitch).then(|| conflicts.push(fullstitch)); + self.remove(&fullstitch).inspect(|&fs| conflicts.push(fs)); conflicts } @@ -179,7 +220,7 @@ impl Stitches { FullStitch { x, kind, ..fullstitch }, FullStitch { y, kind, ..fullstitch }, ] { - self.remove(&petite).then(|| conflicts.push(petite)); + self.remove(&petite).inspect(|&petite| conflicts.push(petite)); } } PartStitchDirection::Backward => { @@ -187,12 +228,12 @@ impl Stitches { FullStitch { kind, ..fullstitch }, FullStitch { x, y, kind, ..fullstitch }, ] { - self.remove(&petite).then(|| conflicts.push(petite)); + self.remove(&petite).inspect(|&petite| conflicts.push(petite)); } } }; - self.remove(&fullstitch).then(|| conflicts.push(fullstitch)); + self.remove(&fullstitch).inspect(|&fs| conflicts.push(fs)); conflicts } @@ -212,7 +253,7 @@ impl Stitches { }, partstitch.to_owned().into(), // Petite ] { - self.remove(&fullstitch).then(|| conflicts.push(fullstitch)); + self.remove(&fullstitch).inspect(|&fs| conflicts.push(fs)); } conflicts @@ -264,7 +305,7 @@ impl Stitches { ..partstitch }, ] { - self.remove(&partstitch).then(|| conflicts.push(partstitch)); + self.remove(&partstitch).inspect(|&ps| conflicts.push(ps)); } conflicts @@ -288,7 +329,7 @@ impl Stitches { direction, kind: PartStitchKind::Half, }; - self.remove(&half).then(|| conflicts.push(half)); + self.remove(&half).inspect(|&half| conflicts.push(half)); let quarter = PartStitch { x, @@ -297,7 +338,7 @@ impl Stitches { direction, kind: PartStitchKind::Quarter, }; - self.remove(&quarter).then(|| conflicts.push(quarter)); + self.remove(&quarter).inspect(|&quarter| conflicts.push(quarter)); conflicts } @@ -328,7 +369,7 @@ impl Stitches { ..*partstitch }, ] { - self.remove(&quarter).then(|| conflicts.push(quarter)); + self.remove(&quarter).inspect(|&quarter| conflicts.push(quarter)); } } PartStitchDirection::Backward => { @@ -346,7 +387,7 @@ impl Stitches { ..*partstitch }, ] { - self.remove(&quarter).then(|| conflicts.push(quarter)); + self.remove(&quarter).inspect(|&quarter| conflicts.push(quarter)); } } } @@ -367,7 +408,7 @@ impl Stitches { direction: PartStitchDirection::from((partstitch.x, partstitch.y)), kind: PartStitchKind::Half, }; - self.remove(&half).then(|| conflicts.push(half)); + self.remove(&half).inspect(|&half| conflicts.push(half)); conflicts } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1977081..fb37129 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -41,6 +41,8 @@ fn main() { commands::pattern::get_pattern_file_path, commands::palette::add_palette_item, commands::stitches::add_stitch, + commands::history::undo, + commands::history::redo, ]) .run(tauri::generate_context!()) .expect("Error while running Embroidery Studio"); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 4f538eb..936c4e4 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -15,10 +15,58 @@ impl From for PatternKey { } } +/// A history of commands. +#[derive(Default)] +pub struct History { + undo_stack: Vec>, + redo_stack: Vec>, +} + +impl History { + /// Add a command to the history. + /// This pushes the command to the undo stack and clears the redo stack. + pub fn push(&mut self, command: Box) { + self.undo_stack.push(command); + self.redo_stack.clear(); + } + + /// Get the last command from the undo stack. + /// This pops the command from the undo stack and pushes it to the redo stack, then returns it. + pub fn undo(&mut self) -> Option> { + self.undo_stack.pop().inspect(|command| { + self.redo_stack.push(command.clone()); + }) + } + + /// Get the last command from the redo stack. + /// This pops the command from the redo stack and pushes it to the undo stack, then returns it. + pub fn redo(&mut self) -> Option> { + self.redo_stack.pop().inspect(|command| { + self.undo_stack.push(command.clone()); + }) + } +} + #[derive(Default)] pub struct AppState { pub patterns: HashMap, - pub history: HashMap>>, + pub history: HashMap, +} + +impl AppState { + /// Insert a pattern into the state. + /// This also initializes the history for the pattern. + pub fn insert_pattern(&mut self, key: PatternKey, patproj: PatternProject) { + self.patterns.insert(key.clone(), patproj); + self.history.insert(key, History::default()); + } + + /// Remove a pattern from the state. + /// This also removes the history for the pattern. + pub fn remove_pattern(&mut self, key: &PatternKey) { + self.patterns.remove(key); + self.history.remove(key); + } } pub type AppStateType = std::sync::RwLock; diff --git a/src/api/history.ts b/src/api/history.ts new file mode 100644 index 0000000..90eb119 --- /dev/null +++ b/src/api/history.ts @@ -0,0 +1,4 @@ +import { invoke } from "@tauri-apps/api/core"; + +export const undo = (patternKey: string) => invoke("undo", { patternKey }); +export const redo = (patternKey: string) => invoke("redo", { patternKey }); diff --git a/src/api/stitches.ts b/src/api/stitches.ts index e052cc9..747248d 100644 --- a/src/api/stitches.ts +++ b/src/api/stitches.ts @@ -1,6 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; import type { FullStitch, Line, Node, PartStitch } from "#/types/pattern/pattern"; -type Stitch = { full: FullStitch } | { part: PartStitch } | { node: Node } | { line: Line }; +export type Stitch = { full: FullStitch } | { part: PartStitch } | { node: Node } | { line: Line }; -export const addStitch = (patternKey: string, stitch: Stitch) => invoke("add_stitch", { patternKey, stitch }); +export const addStitch = (patternKey: string, stitch: Stitch) => invoke("add_stitch", { patternKey, stitch }); diff --git a/src/components/CanvasPanel.vue b/src/components/CanvasPanel.vue index e1b06e7..3dd5ba4 100644 --- a/src/components/CanvasPanel.vue +++ b/src/components/CanvasPanel.vue @@ -5,9 +5,11 @@ From 47de20b148a967ca968984e1fa3ef7ded84df301 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Sun, 17 Nov 2024 11:37:25 +0200 Subject: [PATCH 05/14] test `AddStitchCommand` --- src-tauri/src/commands/history.rs | 4 +- src-tauri/src/commands/stitches.rs | 4 +- src-tauri/src/core/commands/mod.rs | 6 +- src-tauri/src/core/commands/stitches.rs | 10 +- src-tauri/src/core/commands/stitches.test.rs | 111 ++++++++++++++++++ src-tauri/src/core/pattern/display.rs | 18 ++- src-tauri/src/core/pattern/project.rs | 2 +- .../src/core/pattern/stitches/stitches.rs | 4 +- 8 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/core/commands/stitches.test.rs diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index 3f841c3..92a081e 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -6,7 +6,7 @@ use crate::{ }; #[tauri::command] -pub fn undo(window: WebviewWindow, state: tauri::State, pattern_key: PatternKey) -> CommandResult<()> { +pub fn undo(pattern_key: PatternKey, window: WebviewWindow, state: tauri::State) -> CommandResult<()> { let mut state = state.write().unwrap(); let history = state.history.get_mut(&pattern_key).unwrap(); if let Some(command) = history.undo() { @@ -16,7 +16,7 @@ pub fn undo(window: WebviewWindow, state: tauri::State, pattern_ke } #[tauri::command] -pub fn redo(window: WebviewWindow, state: tauri::State, pattern_key: PatternKey) -> CommandResult<()> { +pub fn redo(pattern_key: PatternKey, window: WebviewWindow, state: tauri::State) -> CommandResult<()> { let mut state = state.write().unwrap(); let history = state.history.get_mut(&pattern_key).unwrap(); if let Some(command) = history.redo() { diff --git a/src-tauri/src/commands/stitches.rs b/src-tauri/src/commands/stitches.rs index ee81b47..7a3efcd 100644 --- a/src-tauri/src/commands/stitches.rs +++ b/src-tauri/src/commands/stitches.rs @@ -8,10 +8,10 @@ use crate::{ }; #[tauri::command] -pub fn add_stitch( +pub fn add_stitch( pattern_key: PatternKey, stitch: Stitch, - window: tauri::WebviewWindow, + window: tauri::WebviewWindow, state: tauri::State, ) -> CommandResult<()> { let mut state = state.write().unwrap(); diff --git a/src-tauri/src/core/commands/mod.rs b/src-tauri/src/core/commands/mod.rs index 6f4126a..8af1c1f 100644 --- a/src-tauri/src/core/commands/mod.rs +++ b/src-tauri/src/core/commands/mod.rs @@ -7,18 +7,18 @@ mod stitches; pub use stitches::*; /// A command that can be executed and revoked. -pub trait Command: Send + Sync + dyn_clone::DynClone { +pub trait Command: Send + Sync + dyn_clone::DynClone { /// Execute the command. /// /// The `window` parameter is the webview window that the command should use to emit events. /// The `patproj` parameter is the pattern project that the command should modify. - fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; + fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; /// Revoke the command. /// /// The `window` parameter is the webview window that the command should use to emit events. /// The `patproj` parameter is the pattern project that the command should modify. - fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; + fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; } dyn_clone::clone_trait_object!(Command); diff --git a/src-tauri/src/core/commands/stitches.rs b/src-tauri/src/core/commands/stitches.rs index f1b3fa4..00bb965 100644 --- a/src-tauri/src/core/commands/stitches.rs +++ b/src-tauri/src/core/commands/stitches.rs @@ -7,6 +7,10 @@ use crate::core::pattern::{PatternProject, Stitch, StitchConflicts}; use super::Command; +#[cfg(test)] +#[path = "stitches.test.rs"] +mod tests; + #[derive(Clone)] pub struct AddStitchCommand { stitch: Stitch, @@ -22,8 +26,8 @@ impl AddStitchCommand { } } -impl Command for AddStitchCommand { - fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { +impl Command for AddStitchCommand { + fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { let conflicts = patproj.pattern.add_stitch(self.stitch); if self.conflicts.get().is_none() { self.conflicts.set(conflicts.clone()).unwrap(); @@ -33,7 +37,7 @@ impl Command for AddStitchCommand { Ok(()) } - fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { + fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { let conflicts = self.conflicts.get().unwrap(); patproj.pattern.remove_stitch(self.stitch); for stitch in conflicts.chain() { diff --git a/src-tauri/src/core/commands/stitches.test.rs b/src-tauri/src/core/commands/stitches.test.rs new file mode 100644 index 0000000..15bcd3e --- /dev/null +++ b/src-tauri/src/core/commands/stitches.test.rs @@ -0,0 +1,111 @@ +use ordered_float::NotNan; +use tauri::{ + generate_context, + test::{mock_builder, MockRuntime}, + App, Listener, WebviewUrl, WebviewWindowBuilder, +}; + +use crate::core::{ + commands::Command, + pattern::{ + FullStitch, FullStitchKind, PartStitch, PartStitchDirection, PartStitchKind, PatternProject, Stitch, + StitchConflicts, + }, +}; + +use super::AddStitchCommand; + +pub fn setup_app() -> App { + mock_builder().build(generate_context!()).unwrap() +} + +fn create_pattern_project() -> PatternProject { + let mut patproj = PatternProject::default(); + + // top-left petite + patproj.pattern.fullstitches.insert(FullStitch { + x: NotNan::new(0.0).unwrap(), + y: NotNan::new(0.0).unwrap(), + palindex: 0, + kind: FullStitchKind::Petite, + }); + // top-right quarter + patproj.pattern.partstitches.insert(PartStitch { + x: NotNan::new(0.5).unwrap(), + y: NotNan::new(0.0).unwrap(), + palindex: 0, + kind: PartStitchKind::Quarter, + direction: PartStitchDirection::Forward, + }); + // bottom-left petite + patproj.pattern.fullstitches.insert(FullStitch { + x: NotNan::new(0.0).unwrap(), + y: NotNan::new(0.5).unwrap(), + palindex: 0, + kind: FullStitchKind::Petite, + }); + // bottom-right quarter + patproj.pattern.partstitches.insert(PartStitch { + x: NotNan::new(0.5).unwrap(), + y: NotNan::new(0.5).unwrap(), + palindex: 0, + kind: PartStitchKind::Quarter, + direction: PartStitchDirection::Backward, + }); + + patproj +} + +#[test] +fn test_add_stitch_to_empty_position() { + let app = setup_app(); + let window = WebviewWindowBuilder::new(&app, "main", WebviewUrl::default()) + .build() + .unwrap(); + let patproj = create_pattern_project(); + let stitch = Stitch::Full(FullStitch { + x: NotNan::new(0.0).unwrap(), + y: NotNan::new(0.0).unwrap(), + palindex: 0, + kind: FullStitchKind::Full, + }); + let cmd = AddStitchCommand::new(stitch); + + // Test executing the command. + { + window.listen("stitches:add_one", move |e| { + assert_eq!(serde_json::from_str::(e.payload()).unwrap(), stitch); + }); + + window.listen("stitches:remove_many", |e| { + let conflicts: StitchConflicts = serde_json::from_str(e.payload()).unwrap(); + assert_eq!(conflicts.fullstitches.len(), 2); + assert_eq!(conflicts.partstitches.len(), 2); + }); + + let mut patproj = patproj.clone(); + cmd.execute(&window, &mut patproj).unwrap(); + + assert_eq!(patproj.pattern.fullstitches.len(), 1); + assert_eq!(patproj.pattern.partstitches.len(), 0); + } + + // Test revoking the command. + { + window.listen("stitches:remove_one", move |e| { + assert_eq!(serde_json::from_str::(e.payload()).unwrap(), stitch); + }); + + window.listen("stitches:add_many", |e| { + let conflicts: StitchConflicts = serde_json::from_str(e.payload()).unwrap(); + assert_eq!(conflicts.fullstitches.len(), 2); + assert_eq!(conflicts.partstitches.len(), 2); + }); + + let mut patproj = patproj.clone(); + cmd.revoke(&window, &mut patproj).unwrap(); + + assert_eq!(patproj.pattern.fullstitches.len(), 2); + assert_eq!(patproj.pattern.partstitches.len(), 2); + } +} diff --git a/src-tauri/src/core/pattern/display.rs b/src-tauri/src/core/pattern/display.rs index e42d017..93560e6 100644 --- a/src-tauri/src/core/pattern/display.rs +++ b/src-tauri/src/core/pattern/display.rs @@ -22,13 +22,13 @@ pub struct DisplaySettings { pub stitch_settings: StitchSettings, } -impl DisplaySettings { - pub fn new(palette_size: usize) -> Self { +impl Default for DisplaySettings { + fn default() -> Self { Self { default_stitch_font: String::from("CrossStitch3"), - symbols: vec![Symbols::default(); palette_size], + symbols: Vec::new(), symbol_settings: SymbolSettings::default(), - formats: vec![Formats::default(); palette_size], + formats: Vec::new(), grid: Grid::default(), view: View::Solid, zoom: 100, @@ -44,6 +44,16 @@ impl DisplaySettings { } } +impl DisplaySettings { + pub fn new(palette_size: usize) -> Self { + Self { + symbols: vec![Symbols::default(); palette_size], + formats: vec![Formats::default(); palette_size], + ..DisplaySettings::default() + } + } +} + #[derive(Debug, Default, Clone, PartialEq, BorshSerialize, BorshDeserialize)] pub struct Symbols { pub full: Option, diff --git a/src-tauri/src/core/pattern/project.rs b/src-tauri/src/core/pattern/project.rs index 82a2bc0..b502035 100644 --- a/src-tauri/src/core/pattern/project.rs +++ b/src-tauri/src/core/pattern/project.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use super::{display::DisplaySettings, print::PrintSettings, Pattern}; -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize)] pub struct PatternProject { #[borsh(skip)] pub file_path: std::path::PathBuf, diff --git a/src-tauri/src/core/pattern/stitches/stitches.rs b/src-tauri/src/core/pattern/stitches/stitches.rs index 93c89c4..a109860 100644 --- a/src-tauri/src/core/pattern/stitches/stitches.rs +++ b/src-tauri/src/core/pattern/stitches/stitches.rs @@ -12,7 +12,7 @@ mod tests; pub type Coord = ordered_float::NotNan; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "lowercase")] pub enum Stitch { Full(FullStitch), @@ -94,7 +94,7 @@ impl StitchConflicts { } /// Returns an iterator over all the stitches. - pub fn chain<'a>(&'a self) -> impl Iterator + 'a { + pub fn chain(&self) -> impl Iterator + '_ { self .fullstitches .iter() From a8c72ec48fb16c52228fddc050810a5e9049b685 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Sun, 17 Nov 2024 14:16:09 +0200 Subject: [PATCH 06/14] fix drawing --- src/components/CanvasPanel.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CanvasPanel.vue b/src/components/CanvasPanel.vue index 3dd5ba4..607c2d1 100644 --- a/src/components/CanvasPanel.vue +++ b/src/components/CanvasPanel.vue @@ -34,7 +34,7 @@ // A start point is needed to draw the lines. // An end point is needed to draw all the other kinds of stitches (in addition to lines). canvasService.addEventListener("draw", async (e) => { - if (!appStateStore.state.selectedPaletteItemIndex) return; + if (appStateStore.state.selectedPaletteItemIndex === undefined) return; // @ts-expect-error ... const { start, end, modifier } = e.detail; @@ -114,7 +114,7 @@ // TODO: Don't duplicate this code. canvasService.addEventListener("remove", async (e) => { - if (!appStateStore.state.selectedPaletteItemIndex) return; + if (appStateStore.state.selectedPaletteItemIndex === undefined) return; // @ts-expect-error ... const { point } = e.detail; From ed8773d42ca59666e2370846729cb857497e5143 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Sun, 17 Nov 2024 14:24:15 +0200 Subject: [PATCH 07/14] don't overwright history on pattern loading --- src-tauri/src/commands/pattern.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands/pattern.rs b/src-tauri/src/commands/pattern.rs index 16e170a..54a7986 100644 --- a/src-tauri/src/commands/pattern.rs +++ b/src-tauri/src/commands/pattern.rs @@ -13,11 +13,8 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State { - log::trace!("Pattern has been already loaded"); - pattern.to_owned() - } + let result = match state.patterns.get(&pattern_key) { + Some(pattern) => borsh::to_vec(&pattern)?, None => { let mut new_file_path = file_path.clone(); new_file_path.set_extension(PatternFormat::default().to_string()); @@ -29,14 +26,12 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State Date: Sun, 17 Nov 2024 14:45:06 +0200 Subject: [PATCH 08/14] add a history implementation for the stitch removing --- src-tauri/src/commands/stitches.rs | 16 +++++- src-tauri/src/core/commands/stitches.rs | 25 +++++++++ src-tauri/src/core/commands/stitches.test.rs | 54 +++++++++++++++---- src-tauri/src/events/mod.rs | 1 - src-tauri/src/events/pattern.rs | 55 -------------------- src-tauri/src/lib.rs | 1 - src-tauri/src/main.rs | 6 +-- src/api/stitches.ts | 2 + src/components/CanvasPanel.vue | 11 ++-- 9 files changed, 93 insertions(+), 78 deletions(-) delete mode 100644 src-tauri/src/events/mod.rs delete mode 100644 src-tauri/src/events/pattern.rs diff --git a/src-tauri/src/commands/stitches.rs b/src-tauri/src/commands/stitches.rs index 7a3efcd..acda1c2 100644 --- a/src-tauri/src/commands/stitches.rs +++ b/src-tauri/src/commands/stitches.rs @@ -1,6 +1,6 @@ use crate::{ core::{ - commands::{AddStitchCommand, Command}, + commands::{AddStitchCommand, Command, RemoveStitchCommand}, pattern::Stitch, }, error::CommandResult, @@ -20,3 +20,17 @@ pub fn add_stitch( state.history.get_mut(&pattern_key).unwrap().push(Box::new(command)); Ok(()) } + +#[tauri::command] +pub fn remove_stitch( + pattern_key: PatternKey, + stitch: Stitch, + window: tauri::WebviewWindow, + state: tauri::State, +) -> CommandResult<()> { + let mut state = state.write().unwrap(); + let command = RemoveStitchCommand::new(stitch); + command.execute(&window, state.patterns.get_mut(&pattern_key).unwrap())?; + state.history.get_mut(&pattern_key).unwrap().push(Box::new(command)); + Ok(()) +} diff --git a/src-tauri/src/core/commands/stitches.rs b/src-tauri/src/core/commands/stitches.rs index 00bb965..a70bc2d 100644 --- a/src-tauri/src/core/commands/stitches.rs +++ b/src-tauri/src/core/commands/stitches.rs @@ -48,3 +48,28 @@ impl Command for AddStitchCommand { Ok(()) } } + +#[derive(Clone)] +pub struct RemoveStitchCommand { + stitch: Stitch, +} + +impl RemoveStitchCommand { + pub fn new(stitch: Stitch) -> Self { + Self { stitch } + } +} + +impl Command for RemoveStitchCommand { + fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { + patproj.pattern.remove_stitch(self.stitch); + window.emit("stitches:remove_one", self.stitch)?; + Ok(()) + } + + fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { + patproj.pattern.add_stitch(self.stitch); + window.emit("stitches:add_one", self.stitch)?; + Ok(()) + } +} diff --git a/src-tauri/src/core/commands/stitches.test.rs b/src-tauri/src/core/commands/stitches.test.rs index 15bcd3e..70f6e7f 100644 --- a/src-tauri/src/core/commands/stitches.test.rs +++ b/src-tauri/src/core/commands/stitches.test.rs @@ -5,15 +5,9 @@ use tauri::{ App, Listener, WebviewUrl, WebviewWindowBuilder, }; -use crate::core::{ - commands::Command, - pattern::{ - FullStitch, FullStitchKind, PartStitch, PartStitchDirection, PartStitchKind, PatternProject, Stitch, - StitchConflicts, - }, -}; +use crate::core::{commands::Command, pattern::*}; -use super::AddStitchCommand; +use super::{AddStitchCommand, RemoveStitchCommand}; pub fn setup_app() -> App { mock_builder().build(generate_context!()).unwrap() @@ -57,7 +51,7 @@ fn create_pattern_project() -> PatternProject { } #[test] -fn test_add_stitch_to_empty_position() { +fn test_add_stitch() { let app = setup_app(); let window = WebviewWindowBuilder::new(&app, "main", WebviewUrl::default()) .build() @@ -109,3 +103,45 @@ fn test_add_stitch_to_empty_position() { assert_eq!(patproj.pattern.partstitches.len(), 2); } } + +#[test] +fn test_remove_stitch() { + let app = setup_app(); + let window = WebviewWindowBuilder::new(&app, "main", WebviewUrl::default()) + .build() + .unwrap(); + let patproj = create_pattern_project(); + let stitch = Stitch::Full(FullStitch { + x: NotNan::new(0.0).unwrap(), + y: NotNan::new(0.0).unwrap(), + palindex: 0, + kind: FullStitchKind::Petite, + }); + let cmd = RemoveStitchCommand::new(stitch); + + // Test executing the command. + { + window.listen("stitches:remove_one", move |e| { + assert_eq!(serde_json::from_str::(e.payload()).unwrap(), stitch); + }); + + let mut patproj = patproj.clone(); + cmd.execute(&window, &mut patproj).unwrap(); + + assert_eq!(patproj.pattern.fullstitches.len(), 1); + assert_eq!(patproj.pattern.partstitches.len(), 2); + } + + // Test revoking the command. + { + window.listen("stitches:add_one", move |e| { + assert_eq!(serde_json::from_str::(e.payload()).unwrap(), stitch); + }); + + let mut patproj = patproj.clone(); + cmd.revoke(&window, &mut patproj).unwrap(); + + assert_eq!(patproj.pattern.fullstitches.len(), 2); + assert_eq!(patproj.pattern.partstitches.len(), 2); + } +} diff --git a/src-tauri/src/events/mod.rs b/src-tauri/src/events/mod.rs deleted file mode 100644 index def5479..0000000 --- a/src-tauri/src/events/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod pattern; diff --git a/src-tauri/src/events/pattern.rs b/src-tauri/src/events/pattern.rs deleted file mode 100644 index 7b9e3d4..0000000 --- a/src-tauri/src/events/pattern.rs +++ /dev/null @@ -1,55 +0,0 @@ -use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Emitter, Listener, Manager, WebviewWindow}; - -use crate::{ - core::pattern::{Stitch, StitchConflicts}, - state::{AppStateType, PatternKey}, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct EventStitchPayload { - pattern_key: PatternKey, - payload: T, -} - -static EVENT_STITCH_CREATE: &str = "pattern:stitch:create"; -static EVENT_STITCH_REMOVE: &str = "pattern:stitch:remove"; - -pub fn setup_event_handlers(window: &WebviewWindow, app_handle: &AppHandle) { - log::trace!("Setting up pattern event handlers"); - - let win = window.clone(); - let handle = app_handle.clone(); - window.clone().listen(EVENT_STITCH_CREATE, move |e| { - log::trace!("Received stitch create event"); - let state = handle.state::(); - let mut state = state.write().unwrap(); - - let EventStitchPayload { pattern_key, payload } = - serde_json::from_str::>(e.payload()).unwrap(); - // This is safe because the event is only emitted when the pattern exists. - let pattern = state.patterns.get_mut(&pattern_key).unwrap(); - - emit_remove_stitches(&win, pattern_key, pattern.pattern.add_stitch(payload)); - }); - - let handle = app_handle.clone(); - window.clone().listen(EVENT_STITCH_REMOVE, move |e| { - log::trace!("Received stitch remove event"); - let state = handle.state::(); - let mut state = state.write().unwrap(); - - let EventStitchPayload { pattern_key, payload } = - serde_json::from_str::>(e.payload()).unwrap(); - // This is safe because the event is only emitted when the pattern exists. - let pattern = state.patterns.get_mut(&pattern_key).unwrap(); - pattern.pattern.remove_stitch(payload); - }); -} - -fn emit_remove_stitches(window: &WebviewWindow, pattern_key: PatternKey, payload: StitchConflicts) { - log::trace!("Emitting remove stitches event"); - let payload = EventStitchPayload { pattern_key, payload }; - window.emit("pattern:stitches:remove", payload).unwrap(); -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 98b70f8..54ceb7e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,6 @@ mod error; pub mod commands; -pub mod events; pub mod state; pub mod core; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fb37129..0d0831e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,7 +5,7 @@ use std::{fs, sync::RwLock}; use tauri::Manager; -use embroidery_studio::{commands, events, logger, state, utils}; +use embroidery_studio::{commands, logger, state, utils}; fn main() { tauri::Builder::default() @@ -23,9 +23,6 @@ fn main() { fs::copy(pattern.clone(), app_document_dir.join(pattern.file_name().unwrap()))?; } } - - events::pattern::setup_event_handlers(&app.get_webview_window("main").unwrap(), app.handle()); - Ok(()) }) .manage(RwLock::new(state::AppState::default())) @@ -41,6 +38,7 @@ fn main() { commands::pattern::get_pattern_file_path, commands::palette::add_palette_item, commands::stitches::add_stitch, + commands::stitches::remove_stitch, commands::history::undo, commands::history::redo, ]) diff --git a/src/api/stitches.ts b/src/api/stitches.ts index 747248d..197ae04 100644 --- a/src/api/stitches.ts +++ b/src/api/stitches.ts @@ -4,3 +4,5 @@ import type { FullStitch, Line, Node, PartStitch } from "#/types/pattern/pattern export type Stitch = { full: FullStitch } | { part: PartStitch } | { node: Node } | { line: Line }; export const addStitch = (patternKey: string, stitch: Stitch) => invoke("add_stitch", { patternKey, stitch }); +export const removeStitch = (patternKey: string, stitch: Stitch) => + invoke("remove_stitch", { patternKey, stitch }); diff --git a/src/components/CanvasPanel.vue b/src/components/CanvasPanel.vue index 607c2d1..ee4cba5 100644 --- a/src/components/CanvasPanel.vue +++ b/src/components/CanvasPanel.vue @@ -127,7 +127,7 @@ const ydp = point.y - y; // The current pattern is always available here. - // const patternKey = appStateStore.state.currentPattern!.key; + const patternKey = appStateStore.state.currentPattern!.key; const palindex = appStateStore.state.selectedPaletteItemIndex; const tool = appStateStore.state.selectedStitchTool; @@ -141,8 +141,7 @@ palindex, kind, }; - // await emitStitchRemoved(patternKey, { full: fullstitch }); - canvasService.removeFullStitch(fullstitch); + await stitchesApi.removeStitch(patternKey, { full: fullstitch }); break; } @@ -159,8 +158,7 @@ kind, direction, }; - // await emitStitchRemoved(patternKey, { part: partstitch }); - canvasService.removePartStitch(partstitch); + await stitchesApi.removeStitch(patternKey, { part: partstitch }); break; } @@ -173,8 +171,7 @@ kind, rotated: false, }; - // await emitStitchRemoved(patternKey, { node }); - canvasService.removeNode(node); + await stitchesApi.removeStitch(patternKey, { node }); break; } } From c2e861c942983c79fccce02494f4265687e99f69 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Tue, 19 Nov 2024 16:28:38 +0200 Subject: [PATCH 09/14] move app setup function to the `lib.rs` and split the state to several instances --- src-tauri/src/commands/history.rs | 32 +++++++++++------ src-tauri/src/commands/palette.rs | 8 ++--- src-tauri/src/commands/pattern.rs | 35 +++++++++---------- src-tauri/src/commands/stitches.rs | 22 +++++++----- src-tauri/src/core/commands/mod.rs | 4 +-- src-tauri/src/lib.rs | 54 ++++++++++++++++++++++++++--- src-tauri/src/main.rs | 44 ++---------------------- src-tauri/src/state.rs | 55 +++++++++++++++++------------- src-tauri/tests/pattern.rs | 48 +++++++++++++------------- src-tauri/tests/utils/mod.rs | 14 -------- 10 files changed, 164 insertions(+), 152 deletions(-) delete mode 100644 src-tauri/tests/utils/mod.rs diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index 92a081e..d46dcd8 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -2,25 +2,35 @@ use tauri::WebviewWindow; use crate::{ error::CommandResult, - state::{AppStateType, PatternKey}, + state::{HistoryState, PatternKey, PatternsState}, }; #[tauri::command] -pub fn undo(pattern_key: PatternKey, window: WebviewWindow, state: tauri::State) -> CommandResult<()> { - let mut state = state.write().unwrap(); - let history = state.history.get_mut(&pattern_key).unwrap(); - if let Some(command) = history.undo() { - command.revoke(&window, state.patterns.get_mut(&pattern_key).unwrap())?; +pub fn undo( + pattern_key: PatternKey, + window: WebviewWindow, + history: tauri::State>, + patterns: tauri::State, +) -> CommandResult<()> { + let mut history = history.write().unwrap(); + let mut patterns = patterns.write().unwrap(); + if let Some(command) = history.get_mut(&pattern_key).undo() { + command.revoke(&window, patterns.get_mut(&pattern_key).unwrap())?; } Ok(()) } #[tauri::command] -pub fn redo(pattern_key: PatternKey, window: WebviewWindow, state: tauri::State) -> CommandResult<()> { - let mut state = state.write().unwrap(); - let history = state.history.get_mut(&pattern_key).unwrap(); - if let Some(command) = history.redo() { - command.execute(&window, state.patterns.get_mut(&pattern_key).unwrap())?; +pub fn redo( + pattern_key: PatternKey, + window: WebviewWindow, + history: tauri::State>, + patterns: tauri::State, +) -> CommandResult<()> { + let mut history = history.write().unwrap(); + let mut patterns = patterns.write().unwrap(); + if let Some(command) = history.get_mut(&pattern_key).redo() { + command.execute(&window, patterns.get_mut(&pattern_key).unwrap())?; } Ok(()) } diff --git a/src-tauri/src/commands/palette.rs b/src-tauri/src/commands/palette.rs index 979b1bd..2653b5e 100644 --- a/src-tauri/src/commands/palette.rs +++ b/src-tauri/src/commands/palette.rs @@ -1,11 +1,11 @@ use crate::{ core::pattern::PaletteItem, - state::{AppStateType, PatternKey}, + state::{PatternKey, PatternsState}, }; #[tauri::command] -pub fn add_palette_item(pattern_key: PatternKey, palette_item: PaletteItem, state: tauri::State) { - let mut state = state.write().unwrap(); - let patproj = state.patterns.get_mut(&pattern_key).unwrap(); +pub fn add_palette_item(pattern_key: PatternKey, palette_item: PaletteItem, patterns: tauri::State) { + let mut patterns = patterns.write().unwrap(); + let patproj = patterns.get_mut(&pattern_key).unwrap(); patproj.pattern.palette.push(palette_item); } diff --git a/src-tauri/src/commands/pattern.rs b/src-tauri/src/commands/pattern.rs index 54a7986..2d38677 100644 --- a/src-tauri/src/commands/pattern.rs +++ b/src-tauri/src/commands/pattern.rs @@ -4,17 +4,17 @@ use crate::{ pattern::{display::DisplaySettings, print::PrintSettings, Pattern, PatternProject}, }, error::CommandResult, - state::{AppStateType, PatternKey}, + state::{PatternKey, PatternsState}, utils::path::app_document_dir, }; #[tauri::command] -pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State) -> CommandResult> { +pub fn load_pattern(file_path: std::path::PathBuf, patterns: tauri::State) -> CommandResult> { log::trace!("Loading pattern"); - let mut state = state.write().unwrap(); + let mut patterns = patterns.write().unwrap(); let pattern_key = PatternKey::from(file_path.clone()); - let result = match state.patterns.get(&pattern_key) { - Some(pattern) => borsh::to_vec(&pattern)?, + let result = match patterns.get(&pattern_key) { + Some(pattern) => borsh::to_vec(pattern)?, None => { let mut new_file_path = file_path.clone(); new_file_path.set_extension(PatternFormat::default().to_string()); @@ -27,7 +27,7 @@ pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State( app_handle: tauri::AppHandle, - state: tauri::State, + patterns: tauri::State, ) -> CommandResult<(PatternKey, Vec)> { log::trace!("Creating new pattern"); - let mut state = state.write().unwrap(); + let mut patterns = patterns.write().unwrap(); let pattern = Pattern::default(); let patproj = PatternProject { @@ -55,7 +55,7 @@ pub fn create_pattern( // It is safe to unwrap here, because the pattern is always serializable. let result = (pattern_key.clone(), borsh::to_vec(&patproj).unwrap()); - state.insert_pattern(pattern_key, patproj); + patterns.insert(pattern_key, patproj); log::trace!("Pattern has been created"); Ok(result) @@ -65,11 +65,11 @@ pub fn create_pattern( pub fn save_pattern( pattern_key: PatternKey, file_path: std::path::PathBuf, - state: tauri::State, + patterns: tauri::State, ) -> CommandResult<()> { log::trace!("Saving pattern"); - let mut state = state.write().unwrap(); - let patproj = state.patterns.get_mut(&pattern_key).unwrap(); + let mut patterns = patterns.write().unwrap(); + let patproj = patterns.get_mut(&pattern_key).unwrap(); patproj.file_path = file_path; match PatternFormat::try_from(patproj.file_path.extension())? { PatternFormat::Xsd => Err(anyhow::anyhow!("The XSD format is not supported for saving.")), @@ -81,16 +81,15 @@ pub fn save_pattern( } #[tauri::command] -pub fn close_pattern(pattern_key: PatternKey, state: tauri::State) { +pub fn close_pattern(pattern_key: PatternKey, patterns: tauri::State) { log::trace!("Closing pattern {:?}", pattern_key); - let mut state = state.write().unwrap(); - state.remove_pattern(&pattern_key); + patterns.write().unwrap().remove(&pattern_key); log::trace!("Pattern closed"); } #[tauri::command] -pub fn get_pattern_file_path(pattern_key: PatternKey, state: tauri::State) -> String { - let state = state.read().unwrap(); - let patproj = state.patterns.get(&pattern_key).unwrap(); +pub fn get_pattern_file_path(pattern_key: PatternKey, patterns: tauri::State) -> String { + let patterns = patterns.read().unwrap(); + let patproj = patterns.get(&pattern_key).unwrap(); patproj.file_path.to_string_lossy().to_string() } diff --git a/src-tauri/src/commands/stitches.rs b/src-tauri/src/commands/stitches.rs index acda1c2..27a0a83 100644 --- a/src-tauri/src/commands/stitches.rs +++ b/src-tauri/src/commands/stitches.rs @@ -4,7 +4,7 @@ use crate::{ pattern::Stitch, }, error::CommandResult, - state::{AppStateType, PatternKey}, + state::{HistoryState, PatternKey, PatternsState}, }; #[tauri::command] @@ -12,12 +12,14 @@ pub fn add_stitch( pattern_key: PatternKey, stitch: Stitch, window: tauri::WebviewWindow, - state: tauri::State, + history: tauri::State>, + patterns: tauri::State, ) -> CommandResult<()> { - let mut state = state.write().unwrap(); + let mut history = history.write().unwrap(); + let mut patterns = patterns.write().unwrap(); let command = AddStitchCommand::new(stitch); - command.execute(&window, state.patterns.get_mut(&pattern_key).unwrap())?; - state.history.get_mut(&pattern_key).unwrap().push(Box::new(command)); + command.execute(&window, patterns.get_mut(&pattern_key).unwrap())?; + history.get_mut(&pattern_key).push(Box::new(command)); Ok(()) } @@ -26,11 +28,13 @@ pub fn remove_stitch( pattern_key: PatternKey, stitch: Stitch, window: tauri::WebviewWindow, - state: tauri::State, + history: tauri::State>, + patterns: tauri::State, ) -> CommandResult<()> { - let mut state = state.write().unwrap(); + let mut history = history.write().unwrap(); + let mut patterns = patterns.write().unwrap(); let command = RemoveStitchCommand::new(stitch); - command.execute(&window, state.patterns.get_mut(&pattern_key).unwrap())?; - state.history.get_mut(&pattern_key).unwrap().push(Box::new(command)); + command.execute(&window, patterns.get_mut(&pattern_key).unwrap())?; + history.get_mut(&pattern_key).push(Box::new(command)); Ok(()) } diff --git a/src-tauri/src/core/commands/mod.rs b/src-tauri/src/core/commands/mod.rs index 8af1c1f..3125428 100644 --- a/src-tauri/src/core/commands/mod.rs +++ b/src-tauri/src/core/commands/mod.rs @@ -7,7 +7,7 @@ mod stitches; pub use stitches::*; /// A command that can be executed and revoked. -pub trait Command: Send + Sync + dyn_clone::DynClone { +pub trait Command: Send + Sync + dyn_clone::DynClone { /// Execute the command. /// /// The `window` parameter is the webview window that the command should use to emit events. @@ -21,4 +21,4 @@ pub trait Command: Send + Sync + dyn_clone::DynC fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; } -dyn_clone::clone_trait_object!(Command); +dyn_clone::clone_trait_object!( Command); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 54ceb7e..a5b417a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,9 +1,55 @@ -mod error; +use std::{collections::HashMap, sync::RwLock}; + +use state::HistoryStateInner; +use tauri::Manager; pub mod commands; pub mod state; -pub mod core; -pub mod utils; +mod core; +mod utils; + +mod error; +mod logger; -pub mod logger; +pub fn setup_app(builder: tauri::Builder) -> tauri::App { + builder + .setup(|app| { + let app_document_dir = utils::path::app_document_dir(app.handle())?; + if !cfg!(test) && !app_document_dir.exists() { + // Create the Embroidery Studio directory in the user's document directory + // and copy the sample patterns there if it doesn't exist. + log::debug!("Creating an app document directory",); + std::fs::create_dir(&app_document_dir)?; + log::debug!("Copying sample patterns to the app document directory"); + let resource_path = app.path().resource_dir()?; + for pattern in std::fs::read_dir(resource_path)? { + let pattern = pattern?.path(); + std::fs::copy(pattern.clone(), app_document_dir.join(pattern.file_name().unwrap()))?; + } + } + Ok(()) + }) + .manage(RwLock::new( + HashMap::::new(), + )) + .manage(RwLock::new(HistoryStateInner::::default())) + .plugin(logger::setup_logger().build()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .invoke_handler(tauri::generate_handler![ + commands::path::get_app_document_dir, + commands::pattern::load_pattern, + commands::pattern::create_pattern, + commands::pattern::save_pattern, + commands::pattern::close_pattern, + commands::pattern::get_pattern_file_path, + commands::palette::add_palette_item, + commands::stitches::add_stitch, + commands::stitches::remove_stitch, + commands::history::undo, + commands::history::redo, + ]) + .build(tauri::generate_context!()) + .expect("Failed to build Embroidery Studio") +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0d0831e..6d7e84b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,47 +1,7 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::{fs, sync::RwLock}; - -use tauri::Manager; - -use embroidery_studio::{commands, logger, state, utils}; - fn main() { - tauri::Builder::default() - .setup(|app| { - // Create the Embroidery Studio directory in the user's document directory - // and copy the sample patterns there if it doesn't exist. - let app_document_dir = utils::path::app_document_dir(app.handle())?; - if !app_document_dir.exists() { - log::debug!("Creating an app document directory",); - fs::create_dir(&app_document_dir)?; - log::debug!("Copying sample patterns to the app document directory"); - let resource_path = app.path().resource_dir()?; - for pattern in fs::read_dir(resource_path)? { - let pattern = pattern?.path(); - fs::copy(pattern.clone(), app_document_dir.join(pattern.file_name().unwrap()))?; - } - } - Ok(()) - }) - .manage(RwLock::new(state::AppState::default())) - .plugin(logger::setup_logger().build()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_fs::init()) - .invoke_handler(tauri::generate_handler![ - commands::path::get_app_document_dir, - commands::pattern::load_pattern, - commands::pattern::create_pattern, - commands::pattern::save_pattern, - commands::pattern::close_pattern, - commands::pattern::get_pattern_file_path, - commands::palette::add_palette_item, - commands::stitches::add_stitch, - commands::stitches::remove_stitch, - commands::history::undo, - commands::history::redo, - ]) - .run(tauri::generate_context!()) - .expect("Error while running Embroidery Studio"); + let app = embroidery_studio::setup_app(tauri::Builder::default()); + app.run(|_, _| {}); } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 936c4e4..16b4fd6 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -16,23 +16,22 @@ impl From for PatternKey { } /// A history of commands. -#[derive(Default)] -pub struct History { - undo_stack: Vec>, - redo_stack: Vec>, +pub struct History { + undo_stack: Vec>>, + redo_stack: Vec>>, } -impl History { +impl History { /// Add a command to the history. /// This pushes the command to the undo stack and clears the redo stack. - pub fn push(&mut self, command: Box) { + pub fn push(&mut self, command: Box>) { self.undo_stack.push(command); self.redo_stack.clear(); } /// Get the last command from the undo stack. /// This pops the command from the undo stack and pushes it to the redo stack, then returns it. - pub fn undo(&mut self) -> Option> { + pub fn undo(&mut self) -> Option>> { self.undo_stack.pop().inspect(|command| { self.redo_stack.push(command.clone()); }) @@ -40,33 +39,41 @@ impl History { /// Get the last command from the redo stack. /// This pops the command from the redo stack and pushes it to the undo stack, then returns it. - pub fn redo(&mut self) -> Option> { + pub fn redo(&mut self) -> Option>> { self.redo_stack.pop().inspect(|command| { self.undo_stack.push(command.clone()); }) } } -#[derive(Default)] -pub struct AppState { - pub patterns: HashMap, - pub history: HashMap, +impl Default for History { + fn default() -> Self { + Self { + undo_stack: Vec::new(), + redo_stack: Vec::new(), + } + } +} + +pub struct HistoryStateInner { + inner: HashMap>, } -impl AppState { - /// Insert a pattern into the state. - /// This also initializes the history for the pattern. - pub fn insert_pattern(&mut self, key: PatternKey, patproj: PatternProject) { - self.patterns.insert(key.clone(), patproj); - self.history.insert(key, History::default()); +impl HistoryStateInner { + pub fn get(&self, key: &PatternKey) -> Option<&History> { + self.inner.get(key) } - /// Remove a pattern from the state. - /// This also removes the history for the pattern. - pub fn remove_pattern(&mut self, key: &PatternKey) { - self.patterns.remove(key); - self.history.remove(key); + pub fn get_mut(&mut self, key: &PatternKey) -> &mut History { + self.inner.entry(key.clone()).or_insert_with(History::default) + } +} + +impl Default for HistoryStateInner { + fn default() -> Self { + Self { inner: HashMap::new() } } } -pub type AppStateType = std::sync::RwLock; +pub type PatternsState = std::sync::RwLock>; +pub type HistoryState = std::sync::RwLock>; diff --git a/src-tauri/tests/pattern.rs b/src-tauri/tests/pattern.rs index e697ea9..ee02c3a 100644 --- a/src-tauri/tests/pattern.rs +++ b/src-tauri/tests/pattern.rs @@ -1,12 +1,13 @@ -use tauri::Manager; +use tauri::{ + test::{mock_builder, MockRuntime}, + Manager, +}; use embroidery_studio::{ - commands, - state::{AppStateType, PatternKey}, + commands, setup_app, + state::{PatternKey, PatternsState}, }; -mod utils; - fn get_all_test_patterns() -> Vec> { let sample_patterns = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/patterns"); let test_patterns = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata/patterns"); @@ -18,58 +19,57 @@ fn get_all_test_patterns() -> Vec> { #[test] fn parses_supported_pattern_formats() { - let app = utils::setup_app(); + let app = setup_app::(mock_builder()); let app_handle = app.handle(); - let state = app_handle.state::(); + let patterns_state = app_handle.state::(); for file_path in get_all_test_patterns().into_iter() { let file_path = file_path.unwrap().path(); - assert!(commands::pattern::load_pattern(file_path.clone(), state.clone()).is_ok()); - assert!(state + assert!(commands::pattern::load_pattern(file_path.clone(), patterns_state.clone()).is_ok()); + assert!(patterns_state .read() .unwrap() - .patterns .contains_key(&PatternKey::from(file_path))); } } #[test] fn creates_new_pattern() { - let app = utils::setup_app(); + let app = setup_app::(mock_builder()); let app_handle = app.handle(); - let state = app_handle.state::(); + let patterns_state = app_handle.state::(); - let (pattern_key, _) = commands::pattern::create_pattern(app_handle.clone(), state.clone()).unwrap(); - assert!(state.read().unwrap().patterns.contains_key(&pattern_key)); + let (pattern_key, _) = commands::pattern::create_pattern(app_handle.clone(), patterns_state.clone()).unwrap(); + assert!(patterns_state.read().unwrap().contains_key(&pattern_key)); } #[test] fn saves_pattern() { - let app = utils::setup_app(); + let app = setup_app::(mock_builder()); let app_handle = app.handle(); - let state = app_handle.state::(); + let patterns_state = app_handle.state::(); for file_path in get_all_test_patterns().into_iter() { let file_path = file_path.unwrap().path(); - commands::pattern::load_pattern(file_path.clone(), state.clone()).unwrap(); + commands::pattern::load_pattern(file_path.clone(), patterns_state.clone()).unwrap(); let pattern_key = PatternKey::from(file_path); for extension in ["oxs", "embproj"] { let file_path = std::env::temp_dir().join(format!("pattern.{}", extension)); // If we can save the pattern and then parse it back, we can consider it a success. - assert!(commands::pattern::save_pattern(pattern_key.clone(), file_path.clone(), state.clone()).is_ok()); - assert!(commands::pattern::load_pattern(file_path.clone(), state.clone()).is_ok()); + assert!(commands::pattern::save_pattern(pattern_key.clone(), file_path.clone(), patterns_state.clone()).is_ok()); + assert!(commands::pattern::load_pattern(file_path.clone(), patterns_state.clone()).is_ok()); } } } #[test] fn closes_pattern() { - let app = utils::setup_app(); + let app = setup_app::(mock_builder()); let app_handle = app.handle(); - let state = app_handle.state::(); + let patterns_state = app_handle.state::(); - let (pattern_key, _) = commands::pattern::create_pattern(app_handle.clone(), state.clone()).unwrap(); - commands::pattern::close_pattern(pattern_key.clone(), state.clone()); - assert!(state.read().unwrap().patterns.get(&pattern_key).is_none()); + let (pattern_key, _) = commands::pattern::create_pattern(app_handle.clone(), patterns_state.clone()).unwrap(); + commands::pattern::close_pattern(pattern_key.clone(), patterns_state.clone()); + assert!(patterns_state.read().unwrap().get(&pattern_key).is_none()); } diff --git a/src-tauri/tests/utils/mod.rs b/src-tauri/tests/utils/mod.rs deleted file mode 100644 index 9cee422..0000000 --- a/src-tauri/tests/utils/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use tauri::{ - generate_context, - test::{mock_builder, MockRuntime}, - App, -}; - -use embroidery_studio::state::AppState; - -pub fn setup_app() -> App { - mock_builder() - .manage(std::sync::RwLock::new(AppState::default())) - .build(generate_context!()) - .unwrap() -} From aadd3f7b39508ad4752438309bc7cef7f3e8ae40 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Tue, 19 Nov 2024 18:45:59 +0200 Subject: [PATCH 10/14] rename commands to actions --- src-tauri/src/commands/history.rs | 8 ++--- src-tauri/src/commands/stitches.rs | 14 ++++---- src-tauri/src/core/actions/mod.rs | 18 ++++++++++ .../core/{commands => actions}/stitches.rs | 18 +++++----- .../{commands => actions}/stitches.test.rs | 16 ++++----- src-tauri/src/core/commands/mod.rs | 24 ------------- src-tauri/src/core/mod.rs | 2 +- src-tauri/src/state.rs | 36 +++++++++---------- 8 files changed, 65 insertions(+), 71 deletions(-) create mode 100644 src-tauri/src/core/actions/mod.rs rename src-tauri/src/core/{commands => actions}/stitches.rs (80%) rename src-tauri/src/core/{commands => actions}/stitches.test.rs (90%) delete mode 100644 src-tauri/src/core/commands/mod.rs diff --git a/src-tauri/src/commands/history.rs b/src-tauri/src/commands/history.rs index d46dcd8..6e85f5b 100644 --- a/src-tauri/src/commands/history.rs +++ b/src-tauri/src/commands/history.rs @@ -14,8 +14,8 @@ pub fn undo( ) -> CommandResult<()> { let mut history = history.write().unwrap(); let mut patterns = patterns.write().unwrap(); - if let Some(command) = history.get_mut(&pattern_key).undo() { - command.revoke(&window, patterns.get_mut(&pattern_key).unwrap())?; + if let Some(action) = history.get_mut(&pattern_key).undo() { + action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?; } Ok(()) } @@ -29,8 +29,8 @@ pub fn redo( ) -> CommandResult<()> { let mut history = history.write().unwrap(); let mut patterns = patterns.write().unwrap(); - if let Some(command) = history.get_mut(&pattern_key).redo() { - command.execute(&window, patterns.get_mut(&pattern_key).unwrap())?; + if let Some(action) = history.get_mut(&pattern_key).redo() { + action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?; } Ok(()) } diff --git a/src-tauri/src/commands/stitches.rs b/src-tauri/src/commands/stitches.rs index 27a0a83..0f0e52e 100644 --- a/src-tauri/src/commands/stitches.rs +++ b/src-tauri/src/commands/stitches.rs @@ -1,6 +1,6 @@ use crate::{ core::{ - commands::{AddStitchCommand, Command, RemoveStitchCommand}, + actions::{Action, AddStitchAction, RemoveStitchAction}, pattern::Stitch, }, error::CommandResult, @@ -17,9 +17,9 @@ pub fn add_stitch( ) -> CommandResult<()> { let mut history = history.write().unwrap(); let mut patterns = patterns.write().unwrap(); - let command = AddStitchCommand::new(stitch); - command.execute(&window, patterns.get_mut(&pattern_key).unwrap())?; - history.get_mut(&pattern_key).push(Box::new(command)); + let action = AddStitchAction::new(stitch); + action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?; + history.get_mut(&pattern_key).push(Box::new(action)); Ok(()) } @@ -33,8 +33,8 @@ pub fn remove_stitch( ) -> CommandResult<()> { let mut history = history.write().unwrap(); let mut patterns = patterns.write().unwrap(); - let command = RemoveStitchCommand::new(stitch); - command.execute(&window, patterns.get_mut(&pattern_key).unwrap())?; - history.get_mut(&pattern_key).push(Box::new(command)); + let action = RemoveStitchAction::new(stitch); + action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?; + history.get_mut(&pattern_key).push(Box::new(action)); Ok(()) } diff --git a/src-tauri/src/core/actions/mod.rs b/src-tauri/src/core/actions/mod.rs new file mode 100644 index 0000000..567b33e --- /dev/null +++ b/src-tauri/src/core/actions/mod.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use tauri::WebviewWindow; + +use super::pattern::PatternProject; + +mod stitches; +pub use stitches::*; + +/// An action that can be executed and revoked. +pub trait Action: Send + Sync + dyn_clone::DynClone { + /// Perform the action. + fn perform(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; + + /// Revoke (undo) the action. + fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; +} + +dyn_clone::clone_trait_object!( Action); diff --git a/src-tauri/src/core/commands/stitches.rs b/src-tauri/src/core/actions/stitches.rs similarity index 80% rename from src-tauri/src/core/commands/stitches.rs rename to src-tauri/src/core/actions/stitches.rs index a70bc2d..2d964ad 100644 --- a/src-tauri/src/core/commands/stitches.rs +++ b/src-tauri/src/core/actions/stitches.rs @@ -5,19 +5,19 @@ use tauri::{Emitter, WebviewWindow}; use crate::core::pattern::{PatternProject, Stitch, StitchConflicts}; -use super::Command; +use super::Action; #[cfg(test)] #[path = "stitches.test.rs"] mod tests; #[derive(Clone)] -pub struct AddStitchCommand { +pub struct AddStitchAction { stitch: Stitch, conflicts: OnceLock, } -impl AddStitchCommand { +impl AddStitchAction { pub fn new(stitch: Stitch) -> Self { Self { stitch, @@ -26,8 +26,8 @@ impl AddStitchCommand { } } -impl Command for AddStitchCommand { - fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { +impl Action for AddStitchAction { + fn perform(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { let conflicts = patproj.pattern.add_stitch(self.stitch); if self.conflicts.get().is_none() { self.conflicts.set(conflicts.clone()).unwrap(); @@ -50,18 +50,18 @@ impl Command for AddStitchCommand { } #[derive(Clone)] -pub struct RemoveStitchCommand { +pub struct RemoveStitchAction { stitch: Stitch, } -impl RemoveStitchCommand { +impl RemoveStitchAction { pub fn new(stitch: Stitch) -> Self { Self { stitch } } } -impl Command for RemoveStitchCommand { - fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { +impl Action for RemoveStitchAction { + fn perform(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { patproj.pattern.remove_stitch(self.stitch); window.emit("stitches:remove_one", self.stitch)?; Ok(()) diff --git a/src-tauri/src/core/commands/stitches.test.rs b/src-tauri/src/core/actions/stitches.test.rs similarity index 90% rename from src-tauri/src/core/commands/stitches.test.rs rename to src-tauri/src/core/actions/stitches.test.rs index 70f6e7f..a013be6 100644 --- a/src-tauri/src/core/commands/stitches.test.rs +++ b/src-tauri/src/core/actions/stitches.test.rs @@ -5,9 +5,9 @@ use tauri::{ App, Listener, WebviewUrl, WebviewWindowBuilder, }; -use crate::core::{commands::Command, pattern::*}; +use crate::core::pattern::*; -use super::{AddStitchCommand, RemoveStitchCommand}; +use super::{Action, AddStitchAction, RemoveStitchAction}; pub fn setup_app() -> App { mock_builder().build(generate_context!()).unwrap() @@ -63,7 +63,7 @@ fn test_add_stitch() { palindex: 0, kind: FullStitchKind::Full, }); - let cmd = AddStitchCommand::new(stitch); + let action = AddStitchAction::new(stitch); // Test executing the command. { @@ -78,7 +78,7 @@ fn test_add_stitch() { }); let mut patproj = patproj.clone(); - cmd.execute(&window, &mut patproj).unwrap(); + action.perform(&window, &mut patproj).unwrap(); assert_eq!(patproj.pattern.fullstitches.len(), 1); assert_eq!(patproj.pattern.partstitches.len(), 0); @@ -97,7 +97,7 @@ fn test_add_stitch() { }); let mut patproj = patproj.clone(); - cmd.revoke(&window, &mut patproj).unwrap(); + action.revoke(&window, &mut patproj).unwrap(); assert_eq!(patproj.pattern.fullstitches.len(), 2); assert_eq!(patproj.pattern.partstitches.len(), 2); @@ -117,7 +117,7 @@ fn test_remove_stitch() { palindex: 0, kind: FullStitchKind::Petite, }); - let cmd = RemoveStitchCommand::new(stitch); + let action = RemoveStitchAction::new(stitch); // Test executing the command. { @@ -126,7 +126,7 @@ fn test_remove_stitch() { }); let mut patproj = patproj.clone(); - cmd.execute(&window, &mut patproj).unwrap(); + action.perform(&window, &mut patproj).unwrap(); assert_eq!(patproj.pattern.fullstitches.len(), 1); assert_eq!(patproj.pattern.partstitches.len(), 2); @@ -139,7 +139,7 @@ fn test_remove_stitch() { }); let mut patproj = patproj.clone(); - cmd.revoke(&window, &mut patproj).unwrap(); + action.revoke(&window, &mut patproj).unwrap(); assert_eq!(patproj.pattern.fullstitches.len(), 2); assert_eq!(patproj.pattern.partstitches.len(), 2); diff --git a/src-tauri/src/core/commands/mod.rs b/src-tauri/src/core/commands/mod.rs deleted file mode 100644 index 3125428..0000000 --- a/src-tauri/src/core/commands/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -use anyhow::Result; -use tauri::WebviewWindow; - -use super::pattern::PatternProject; - -mod stitches; -pub use stitches::*; - -/// A command that can be executed and revoked. -pub trait Command: Send + Sync + dyn_clone::DynClone { - /// Execute the command. - /// - /// The `window` parameter is the webview window that the command should use to emit events. - /// The `patproj` parameter is the pattern project that the command should modify. - fn execute(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; - - /// Revoke the command. - /// - /// The `window` parameter is the webview window that the command should use to emit events. - /// The `patproj` parameter is the pattern project that the command should modify. - fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()>; -} - -dyn_clone::clone_trait_object!( Command); diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 597dbab..c0f5335 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,3 @@ -pub mod commands; +pub mod actions; pub mod parser; pub mod pattern; diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 16b4fd6..830ed06 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use crate::core::{commands::Command, pattern::PatternProject}; +use crate::core::{actions::Action, pattern::PatternProject}; #[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[repr(transparent)] @@ -15,33 +15,33 @@ impl From for PatternKey { } } -/// A history of commands. +/// A history of actions. pub struct History { - undo_stack: Vec>>, - redo_stack: Vec>>, + undo_stack: Vec>>, + redo_stack: Vec>>, } impl History { - /// Add a command to the history. - /// This pushes the command to the undo stack and clears the redo stack. - pub fn push(&mut self, command: Box>) { - self.undo_stack.push(command); + /// Add an action object to the history. + /// This pushes the action object to the undo stack and clears the redo stack. + pub fn push(&mut self, action: Box>) { + self.undo_stack.push(action); self.redo_stack.clear(); } - /// Get the last command from the undo stack. - /// This pops the command from the undo stack and pushes it to the redo stack, then returns it. - pub fn undo(&mut self) -> Option>> { - self.undo_stack.pop().inspect(|command| { - self.redo_stack.push(command.clone()); + /// Get the last action object from the undo stack. + /// This pops the action object from the undo stack and pushes it to the redo stack, then returns it. + pub fn undo(&mut self) -> Option>> { + self.undo_stack.pop().inspect(|action| { + self.redo_stack.push(action.clone()); }) } - /// Get the last command from the redo stack. - /// This pops the command from the redo stack and pushes it to the undo stack, then returns it. - pub fn redo(&mut self) -> Option>> { - self.redo_stack.pop().inspect(|command| { - self.undo_stack.push(command.clone()); + /// Get the last action object from the redo stack. + /// This pops the action object from the redo stack and pushes it to the undo stack, then returns it. + pub fn redo(&mut self) -> Option>> { + self.redo_stack.pop().inspect(|action| { + self.undo_stack.push(action.clone()); }) } } From 7caf277b5db400c99712d9276729d253f28a0467 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Tue, 19 Nov 2024 18:48:09 +0200 Subject: [PATCH 11/14] extract the history to a separate module --- src-tauri/src/core/history.rs | 41 ++++++++++++++++++++++++++++++++++ src-tauri/src/core/mod.rs | 1 + src-tauri/src/state.rs | 42 +---------------------------------- 3 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 src-tauri/src/core/history.rs diff --git a/src-tauri/src/core/history.rs b/src-tauri/src/core/history.rs new file mode 100644 index 0000000..c0b06c7 --- /dev/null +++ b/src-tauri/src/core/history.rs @@ -0,0 +1,41 @@ +use super::actions::Action; + +/// A history of actions. +pub struct History { + undo_stack: Vec>>, + redo_stack: Vec>>, +} + +impl History { + /// Add an action object to the history. + /// This pushes the action object to the undo stack and clears the redo stack. + pub fn push(&mut self, action: Box>) { + self.undo_stack.push(action); + self.redo_stack.clear(); + } + + /// Get the last action object from the undo stack. + /// This pops the action object from the undo stack and pushes it to the redo stack, then returns it. + pub fn undo(&mut self) -> Option>> { + self.undo_stack.pop().inspect(|action| { + self.redo_stack.push(action.clone()); + }) + } + + /// Get the last action object from the redo stack. + /// This pops the action object from the redo stack and pushes it to the undo stack, then returns it. + pub fn redo(&mut self) -> Option>> { + self.redo_stack.pop().inspect(|action| { + self.undo_stack.push(action.clone()); + }) + } +} + +impl Default for History { + fn default() -> Self { + Self { + undo_stack: Vec::new(), + redo_stack: Vec::new(), + } + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index c0f5335..5b2e5ff 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,4 @@ pub mod actions; +pub mod history; pub mod parser; pub mod pattern; diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 830ed06..38c930c 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf}; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use crate::core::{actions::Action, pattern::PatternProject}; +use crate::core::{history::History, pattern::PatternProject}; #[derive(Debug, Hash, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[repr(transparent)] @@ -15,46 +15,6 @@ impl From for PatternKey { } } -/// A history of actions. -pub struct History { - undo_stack: Vec>>, - redo_stack: Vec>>, -} - -impl History { - /// Add an action object to the history. - /// This pushes the action object to the undo stack and clears the redo stack. - pub fn push(&mut self, action: Box>) { - self.undo_stack.push(action); - self.redo_stack.clear(); - } - - /// Get the last action object from the undo stack. - /// This pops the action object from the undo stack and pushes it to the redo stack, then returns it. - pub fn undo(&mut self) -> Option>> { - self.undo_stack.pop().inspect(|action| { - self.redo_stack.push(action.clone()); - }) - } - - /// Get the last action object from the redo stack. - /// This pops the action object from the redo stack and pushes it to the undo stack, then returns it. - pub fn redo(&mut self) -> Option>> { - self.redo_stack.pop().inspect(|action| { - self.undo_stack.push(action.clone()); - }) - } -} - -impl Default for History { - fn default() -> Self { - Self { - undo_stack: Vec::new(), - redo_stack: Vec::new(), - } - } -} - pub struct HistoryStateInner { inner: HashMap>, } From b2ce6d86b2c4291440adf706252233030f61aab4 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Tue, 19 Nov 2024 19:18:45 +0200 Subject: [PATCH 12/14] update documentation --- DEVELOPMENT.md | 20 +++++++++++--------- src-tauri/src/core/actions/mod.rs | 10 ++++++++++ src-tauri/src/core/actions/stitches.rs | 19 +++++++++++++++++++ src-tauri/src/core/history.rs | 3 +++ 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7df9d08..f57304a 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -14,7 +14,7 @@ src/ # Everything related to the frontend. ├── schemas/ # Schemas and types for parsing borsh-serialized data. ├── services/ # Modules that encapsulate complex logic. ├── stores/ # Pinia stores to share some application state through components. -├── types/ # Types definition. +├── types/ # Type definitions. ├── utils/ # A set of utility functions. ├── App.vue # The main application component. └── main.ts # An entry point for the entire application. @@ -23,17 +23,19 @@ src-tauri/ # Everything related to the backend. ├── icons/ # Desktop icons. ├── resources/ # Sample patterns, stitch fonts, colour palettes, etc. ├── src/ # Application source code. -│ ├── commands/ # A set of commands exposed to the frontend. -│ ├── events/ # Event handles. -│ ├── parser/ # Cross-stitch pattern files parsers. -│ │ ├── oxs/ # OXS parser. -│ │ └── xsd.rs # XSD parser. -│ ├── pattern/ # Pattern structure definition that is used internally. -│ │ └── stitches/ # Definitions of the various stitch kinds and their methods. +│ ├── commands/ # A set of Tauri commands exposed to the frontend. +│ ├── core/ # The core functionality. +│ │ ├── actions/ # A set of actions for performing changes to patterns. +│ │ ├── parser/ # Cross-stitch pattern files parsers. +│ │ │ ├── oxs/ # OXS parser. +│ │ │ └── xsd.rs # XSD parser. +│ │ ├── pattern/ # Pattern structure definition that is used internally. +│ │ │ └── stitches/ # Definitions of the various stitch kinds and their methods. +│ │ └── history.rs # Defines a structure to save performed action objects. │ ├── utils/ # A set of utility functions. │ ├── error.rs # Defines custom error type for the command result. │ ├── logger.rs # Configures the Tauri logger plugin. -│ ├── state.rs # Defines the application state. +│ ├── state.rs # Defines the application states. │ ├── lib.rs # Composes all modules into the library used in `main.rs` and `tests/`. │ └── main.rs # Bundles everything together and runs the application. └── tests/ # End-to-end backend tests. diff --git a/src-tauri/src/core/actions/mod.rs b/src-tauri/src/core/actions/mod.rs index 567b33e..49e754b 100644 --- a/src-tauri/src/core/actions/mod.rs +++ b/src-tauri/src/core/actions/mod.rs @@ -1,3 +1,13 @@ +//! This module contains the definition of actions that can be performed on a pattern project. +//! These actions include operations like adding or removing stitches or palette items, updating pattern information, etc. +//! +//! Actually, the actions implements the `Command` pattern. +//! Hovewer we named it `Action` to avoid confusion with the `commands` from Tauri. +//! +//! Each method of the `Action` accepts a reference to the `WebviewWindow` and a mutable reference to the `PatternProject`. +//! The `WebviewWindow` is used to emit events to the frontend. +//! The reason for this is that the `Action` can affects many aspects of the `PatternProject` so it is easier to emit an event for each change. + use anyhow::Result; use tauri::WebviewWindow; diff --git a/src-tauri/src/core/actions/stitches.rs b/src-tauri/src/core/actions/stitches.rs index 2d964ad..ee4d932 100644 --- a/src-tauri/src/core/actions/stitches.rs +++ b/src-tauri/src/core/actions/stitches.rs @@ -19,6 +19,7 @@ pub struct AddStitchAction { impl AddStitchAction { pub fn new(stitch: Stitch) -> Self { + // We need to use the `OnceLock` here because we can't directly mutate the internal state of the action. Self { stitch, conflicts: OnceLock::new(), @@ -27,6 +28,11 @@ impl AddStitchAction { } impl Action for AddStitchAction { + /// Add the stitch to the pattern. + /// + /// **Emits:** + /// - `stitches:add_one` with the added stitch + /// - `stitches:remove_many` with the removed stitches that conflict with the new stitch fn perform(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { let conflicts = patproj.pattern.add_stitch(self.stitch); if self.conflicts.get().is_none() { @@ -37,6 +43,11 @@ impl Action for AddStitchAction { Ok(()) } + /// Remove the added stitch from the pattern. + /// + /// **Emits:** + /// - `stitches:remove_one` with the removed stitch + /// - `stitches:add_many` with the added stitches that were removed when the stitch was added fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { let conflicts = self.conflicts.get().unwrap(); patproj.pattern.remove_stitch(self.stitch); @@ -61,12 +72,20 @@ impl RemoveStitchAction { } impl Action for RemoveStitchAction { + /// Remove the stitch from the pattern. + /// + /// **Emits:** + /// - `stitches:remove_one` with the removed stitch fn perform(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { patproj.pattern.remove_stitch(self.stitch); window.emit("stitches:remove_one", self.stitch)?; Ok(()) } + /// Add the removed stitch back to the pattern. + /// + /// **Emits:** + /// - `stitches:add_one` with the added stitch fn revoke(&self, window: &WebviewWindow, patproj: &mut PatternProject) -> Result<()> { patproj.pattern.add_stitch(self.stitch); window.emit("stitches:add_one", self.stitch)?; diff --git a/src-tauri/src/core/history.rs b/src-tauri/src/core/history.rs index c0b06c7..5cb4c4d 100644 --- a/src-tauri/src/core/history.rs +++ b/src-tauri/src/core/history.rs @@ -1,3 +1,6 @@ +//! This module contains the definition of a history of actions. +//! The history is stored per pattern project. + use super::actions::Action; /// A history of actions. From e12637b1ba0ca48280dd39872c7b8397813ce437 Mon Sep 17 00:00:00 2001 From: Nazar Anroniuk Date: Wed, 20 Nov 2024 14:32:59 +0200 Subject: [PATCH 13/14] test history --- src-tauri/src/core/actions/mod.rs | 14 +++++++++ src-tauri/src/core/history.rs | 4 +++ src-tauri/src/core/history.test.rs | 49 ++++++++++++++++++++++++++++++ src-tauri/src/state.rs | 2 +- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src-tauri/src/core/history.test.rs diff --git a/src-tauri/src/core/actions/mod.rs b/src-tauri/src/core/actions/mod.rs index 49e754b..568e711 100644 --- a/src-tauri/src/core/actions/mod.rs +++ b/src-tauri/src/core/actions/mod.rs @@ -26,3 +26,17 @@ pub trait Action: Send + Sync + dyn_clone::DynClone { } dyn_clone::clone_trait_object!( Action); + +#[cfg(debug_assertions)] +#[derive(Clone)] +pub struct MockAction; + +impl Action for MockAction { + fn perform(&self, _window: &WebviewWindow, _patproj: &mut PatternProject) -> Result<()> { + Ok(()) + } + + fn revoke(&self, _window: &WebviewWindow, _patproj: &mut PatternProject) -> Result<()> { + Ok(()) + } +} diff --git a/src-tauri/src/core/history.rs b/src-tauri/src/core/history.rs index 5cb4c4d..7946e62 100644 --- a/src-tauri/src/core/history.rs +++ b/src-tauri/src/core/history.rs @@ -3,6 +3,10 @@ use super::actions::Action; +#[cfg(test)] +#[path = "history.test.rs"] +mod tests; + /// A history of actions. pub struct History { undo_stack: Vec>>, diff --git a/src-tauri/src/core/history.test.rs b/src-tauri/src/core/history.test.rs new file mode 100644 index 0000000..c6458c9 --- /dev/null +++ b/src-tauri/src/core/history.test.rs @@ -0,0 +1,49 @@ +use tauri::test::MockRuntime; + +use super::History; +use crate::core::actions::MockAction; + +#[test] +fn test_push() { + let mut history = History::::default(); + + history.push(Box::new(MockAction)); + assert_eq!(history.undo_stack.len(), 1); + assert_eq!(history.redo_stack.len(), 0); + + history.push(Box::new(MockAction)); + assert_eq!(history.undo_stack.len(), 2); + assert_eq!(history.redo_stack.len(), 0); +} + +#[test] +fn test_undo() { + let mut history = History::::default(); + + history.push(Box::new(MockAction)); + history.push(Box::new(MockAction)); + assert_eq!(history.undo_stack.len(), 2); + assert_eq!(history.redo_stack.len(), 0); + + assert!(history.undo().is_some()); + assert_eq!(history.undo_stack.len(), 1); + assert_eq!(history.redo_stack.len(), 1); + + assert!(history.undo().is_some()); + assert_eq!(history.undo_stack.len(), 0); + assert_eq!(history.redo_stack.len(), 2); + assert!(history.undo().is_none()); +} + +#[test] +fn test_redo() { + let mut history = History::::default(); + history.push(Box::new(MockAction)); + history.push(Box::new(MockAction)); + history.undo(); + + assert!(history.redo().is_some()); + assert_eq!(history.undo_stack.len(), 2); + assert_eq!(history.redo_stack.len(), 0); + assert!(history.redo().is_none()); +} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 38c930c..903df71 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -25,7 +25,7 @@ impl HistoryStateInner { } pub fn get_mut(&mut self, key: &PatternKey) -> &mut History { - self.inner.entry(key.clone()).or_insert_with(History::default) + self.inner.entry(key.clone()).or_default() } } From 995165bdd47ec5eb954b95703984da178ee3e1bb Mon Sep 17 00:00:00 2001 From: Nazar Antoniuk Date: Sun, 24 Nov 2024 09:17:56 +0200 Subject: [PATCH 14/14] use `cargo-nextest` for running tests --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecd38f4..b450c4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,9 +43,10 @@ jobs: - uses: swatinem/rust-cache@v2 with: workspaces: "./src-tauri -> target" + - uses: taiki-e/install-action@nextest - name: Check formatting run: cargo fmt --check - name: Lint run: cargo clippy -- -D warnings - name: Test - run: cargo test + run: cargo nextest run