Skip to content

Commit

Permalink
feat: add an actions history implementation (#34)
Browse files Browse the repository at this point in the history
* add an initial feature outline using the `Command` pattern

* add shortcuts system

* move palette command to a separate module

* add a basic history implementation for the stitch creation

* test `AddStitchCommand`

* fix drawing

* don't overwright history on pattern loading

* add a history implementation for the stitch removing

* move app setup function to the `lib.rs` and split the state to several instances

* rename commands to actions

* extract the history to a separate module

* update documentation

* test history

* use `cargo-nextest` for running tests
  • Loading branch information
niusia-ua authored Nov 24, 2024
1 parent 147da0b commit acb748d
Show file tree
Hide file tree
Showing 42 changed files with 1,013 additions and 524 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 11 additions & 9 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ log = "0.4.22"

# Other
ordered-float = { version = "4.5.0", features = ["borsh", "serde"] }
dyn-clone = "1.0.17"
36 changes: 36 additions & 0 deletions src-tauri/src/commands/history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use tauri::WebviewWindow;

use crate::{
error::CommandResult,
state::{HistoryState, PatternKey, PatternsState},
};

#[tauri::command]
pub fn undo<R: tauri::Runtime>(
pattern_key: PatternKey,
window: WebviewWindow<R>,
history: tauri::State<HistoryState<R>>,
patterns: tauri::State<PatternsState>,
) -> CommandResult<()> {
let mut history = history.write().unwrap();
let mut patterns = patterns.write().unwrap();
if let Some(action) = history.get_mut(&pattern_key).undo() {
action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?;
}
Ok(())
}

#[tauri::command]
pub fn redo<R: tauri::Runtime>(
pattern_key: PatternKey,
window: WebviewWindow<R>,
history: tauri::State<HistoryState<R>>,
patterns: tauri::State<PatternsState>,
) -> CommandResult<()> {
let mut history = history.write().unwrap();
let mut patterns = patterns.write().unwrap();
if let Some(action) = history.get_mut(&pattern_key).redo() {
action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?;
}
Ok(())
}
3 changes: 3 additions & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
pub mod history;
pub mod palette;
pub mod path;
pub mod pattern;
pub mod stitches;
11 changes: 11 additions & 0 deletions src-tauri/src/commands/palette.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use crate::{
core::pattern::PaletteItem,
state::{PatternKey, PatternsState},
};

#[tauri::command]
pub fn add_palette_item(pattern_key: PatternKey, palette_item: PaletteItem, patterns: tauri::State<PatternsState>) {
let mut patterns = patterns.write().unwrap();
let patproj = patterns.get_mut(&pattern_key).unwrap();
patproj.pattern.palette.push(palette_item);
}
56 changes: 22 additions & 34 deletions src-tauri/src/commands/pattern.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,47 @@
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},
state::{PatternKey, PatternsState},
utils::path::app_document_dir,
};

#[tauri::command]
pub fn load_pattern(file_path: std::path::PathBuf, state: tauri::State<AppStateType>) -> CommandResult<Vec<u8>> {
pub fn load_pattern(file_path: std::path::PathBuf, patterns: tauri::State<PatternsState>) -> CommandResult<Vec<u8>> {
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 pattern = match state.patterns.get(&pattern_key) {
Some(pattern) => {
log::trace!("Pattern has been already loaded");
pattern.to_owned()
}
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());

let mut pattern = match PatternFormat::try_from(file_path.extension())? {
let mut patproj = match PatternFormat::try_from(file_path.extension())? {
PatternFormat::Xsd => 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
let result = borsh::to_vec(&patproj)?;
patterns.insert(pattern_key, patproj);
result
}
};
let result = borsh::to_vec(&pattern)?;

state.patterns.insert(pattern_key, pattern.clone());
log::trace!("Pattern loaded");

Ok(result)
}

#[tauri::command]
pub fn create_pattern<R: tauri::Runtime>(
app_handle: tauri::AppHandle<R>,
state: tauri::State<AppStateType>,
patterns: tauri::State<PatternsState>,
) -> CommandResult<(PatternKey, Vec<u8>)> {
log::trace!("Creating new pattern");
let mut state = state.write().unwrap();
let mut patterns = patterns.write().unwrap();

let pattern = Pattern::default();
let patproj = PatternProject {
Expand All @@ -60,7 +55,7 @@ pub fn create_pattern<R: tauri::Runtime>(
// 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);
patterns.insert(pattern_key, patproj);
log::trace!("Pattern has been created");

Ok(result)
Expand All @@ -70,11 +65,11 @@ pub fn create_pattern<R: tauri::Runtime>(
pub fn save_pattern(
pattern_key: PatternKey,
file_path: std::path::PathBuf,
state: tauri::State<AppStateType>,
patterns: tauri::State<PatternsState>,
) -> 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.")),
Expand All @@ -86,22 +81,15 @@ pub fn save_pattern(
}

#[tauri::command]
pub fn close_pattern(pattern_key: PatternKey, state: tauri::State<AppStateType>) {
pub fn close_pattern(pattern_key: PatternKey, patterns: tauri::State<PatternsState>) {
log::trace!("Closing pattern {:?}", pattern_key);
state.write().unwrap().patterns.remove(&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<AppStateType>) -> 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<PatternsState>) -> String {
let patterns = patterns.read().unwrap();
let patproj = patterns.get(&pattern_key).unwrap();
patproj.file_path.to_string_lossy().to_string()
}

#[tauri::command]
pub fn add_palette_item(pattern_key: PatternKey, palette_item: PaletteItem, state: tauri::State<AppStateType>) {
let mut state = state.write().unwrap();
let patproj = state.patterns.get_mut(&pattern_key).unwrap();
patproj.pattern.palette.push(palette_item);
}
40 changes: 40 additions & 0 deletions src-tauri/src/commands/stitches.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::{
core::{
actions::{Action, AddStitchAction, RemoveStitchAction},
pattern::Stitch,
},
error::CommandResult,
state::{HistoryState, PatternKey, PatternsState},
};

#[tauri::command]
pub fn add_stitch<R: tauri::Runtime>(
pattern_key: PatternKey,
stitch: Stitch,
window: tauri::WebviewWindow<R>,
history: tauri::State<HistoryState<R>>,
patterns: tauri::State<PatternsState>,
) -> CommandResult<()> {
let mut history = history.write().unwrap();
let mut patterns = patterns.write().unwrap();
let action = AddStitchAction::new(stitch);
action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?;
history.get_mut(&pattern_key).push(Box::new(action));
Ok(())
}

#[tauri::command]
pub fn remove_stitch<R: tauri::Runtime>(
pattern_key: PatternKey,
stitch: Stitch,
window: tauri::WebviewWindow<R>,
history: tauri::State<HistoryState<R>>,
patterns: tauri::State<PatternsState>,
) -> CommandResult<()> {
let mut history = history.write().unwrap();
let mut patterns = patterns.write().unwrap();
let action = RemoveStitchAction::new(stitch);
action.perform(&window, patterns.get_mut(&pattern_key).unwrap())?;
history.get_mut(&pattern_key).push(Box::new(action));
Ok(())
}
42 changes: 42 additions & 0 deletions src-tauri/src/core/actions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! 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;

use super::pattern::PatternProject;

mod stitches;
pub use stitches::*;

/// An action that can be executed and revoked.
pub trait Action<R: tauri::Runtime>: Send + Sync + dyn_clone::DynClone {
/// Perform the action.
fn perform(&self, window: &WebviewWindow<R>, patproj: &mut PatternProject) -> Result<()>;

/// Revoke (undo) the action.
fn revoke(&self, window: &WebviewWindow<R>, patproj: &mut PatternProject) -> Result<()>;
}

dyn_clone::clone_trait_object!(<R: tauri::Runtime> Action<R>);

#[cfg(debug_assertions)]
#[derive(Clone)]
pub struct MockAction;

impl<R: tauri::Runtime> Action<R> for MockAction {
fn perform(&self, _window: &WebviewWindow<R>, _patproj: &mut PatternProject) -> Result<()> {
Ok(())
}

fn revoke(&self, _window: &WebviewWindow<R>, _patproj: &mut PatternProject) -> Result<()> {
Ok(())
}
}
Loading

0 comments on commit acb748d

Please sign in to comment.