Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,769 changes: 1,698 additions & 71 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dialoguer = "0.12.0"
flate2 = "1.1"
fs_extra = "1.3"
futures = "0.3.31"
gdk4 = "0.10.0"
gdk-pixbuf = "0.21.1"
geo = "0.30.0"
gettext-rs = { version = "0.7.2", features = ["gettext-system"] }
gio = "0.21.1"
Expand Down Expand Up @@ -79,6 +81,9 @@ svg = "0.18.0"
thiserror = "2.0.12"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
typst = "0.14.0"
typst-kit = { version = "0.14.0", features = ["embed-fonts"] }
typst-svg = "0.14.0"
unicode-segmentation = "1.12"
url = "2.5"
usvg = "0.45.1"
Expand Down
32 changes: 18 additions & 14 deletions crates/rnote-compose/src/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,21 @@ use anyhow::Context;
use serde::{Deserialize, Serialize};

#[derive(
Copy, Clone, Debug, Serialize, Deserialize, num_derive::FromPrimitive, num_derive::ToPrimitive,
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
num_derive::FromPrimitive,
num_derive::ToPrimitive,
)]
#[serde(rename = "shapebuilder_type")]
/// A choice for a shape builder type
pub enum ShapeBuilderType {
/// A line builder
#[serde(rename = "line")]
#[default]
Line = 0,
/// An arrow builder
#[serde(rename = "arrow")]
Expand Down Expand Up @@ -130,12 +138,6 @@ impl ShapeBuilderType {
}
}

impl Default for ShapeBuilderType {
fn default() -> Self {
Self::Line
}
}

impl TryFrom<u32> for ShapeBuilderType {
type Error = anyhow::Error;

Expand All @@ -146,7 +148,14 @@ impl TryFrom<u32> for ShapeBuilderType {
}

#[derive(
Copy, Clone, Debug, Serialize, Deserialize, num_derive::FromPrimitive, num_derive::ToPrimitive,
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
num_derive::FromPrimitive,
num_derive::ToPrimitive,
)]
#[serde(rename = "penpathbuilder_type")]
/// A choice for a pen path builder type
Expand All @@ -159,15 +168,10 @@ pub enum PenPathBuilderType {
Curved,
#[serde(rename = "modeled")]
/// the modeled pen path builder
#[default]
Modeled,
}

impl Default for PenPathBuilderType {
fn default() -> Self {
Self::Modeled
}
}

impl TryFrom<u32> for PenPathBuilderType {
type Error = anyhow::Error;

Expand Down
16 changes: 9 additions & 7 deletions crates/rnote-compose/src/style/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,14 @@ impl Composer<Style> for Shape {

/// The pressure curve used by some styles.
#[derive(
Debug, Clone, Copy, Serialize, Deserialize, num_derive::FromPrimitive, num_derive::ToPrimitive,
Debug,
Clone,
Copy,
Default,
Serialize,
Deserialize,
num_derive::FromPrimitive,
num_derive::ToPrimitive,
)]
#[serde(rename = "pressure_curve")]
pub enum PressureCurve {
Expand All @@ -320,6 +327,7 @@ pub enum PressureCurve {
Const = 0,
/// Linear.
#[serde(rename = "linear")]
#[default]
Linear,
/// Square root.
#[serde(rename = "sqrt")]
Expand All @@ -335,12 +343,6 @@ pub enum PressureCurve {
Pow3,
}

impl Default for PressureCurve {
fn default() -> Self {
Self::Linear
}
}

impl PressureCurve {
/// Apply the pressure curve to a width and the given pressure.
///
Expand Down
8 changes: 2 additions & 6 deletions crates/rnote-compose/src/style/rough/roughoptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ impl RoughOptions {
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Expand All @@ -75,6 +76,7 @@ pub enum FillStyle {
Solid,
/// Hachure.
#[serde(rename = "hachure")]
#[default]
Hachure,
/// Zig zag.
#[serde(rename = "zig_zag")]
Expand All @@ -93,12 +95,6 @@ pub enum FillStyle {
Dashed,
}

impl Default for FillStyle {
fn default() -> Self {
Self::Hachure
}
}

impl From<roughr::core::FillStyle> for FillStyle {
fn from(s: roughr::core::FillStyle) -> Self {
match s {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::ops::Range;
PartialEq,
Clone,
Copy,
Default,
Serialize,
Deserialize,
num_derive::FromPrimitive,
Expand All @@ -20,19 +21,14 @@ pub enum TexturedDotsDistribution {
/// Uniform distribution.
Uniform = 0,
/// Normal distribution.
#[default]
Normal,
/// Exponential distribution distribution, from the outline increasing in probability symmetrical to the center.
Exponential,
/// Exponential distribution distribution, from the center increasing in probability symmetrical outwards to the outline.
ReverseExponential,
}

impl Default for TexturedDotsDistribution {
fn default() -> Self {
Self::Normal
}
}

impl TryFrom<u32> for TexturedDotsDistribution {
type Error = anyhow::Error;

Expand Down
5 changes: 4 additions & 1 deletion crates/rnote-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ usvg = { workspace = true }
xmlwriter = { workspace = true }
# the long-term plan is to remove the gtk4 dependency entirely after switching to another renderer.
gtk4 = { workspace = true, optional = true }
typst = { workspace = true, optional = true }
typst-kit = { workspace = true, optional = true }
typst-svg = { workspace = true, optional = true }

[dev-dependencies]
approx = { workspace = true }

[features]
cli = ["dep:clap"]
default = []
ui = ["dep:gtk4"]
ui = ["dep:gtk4", "dep:typst", "dep:typst-kit", "dep:typst-svg"]
121 changes: 121 additions & 0 deletions crates/rnote-engine/src/engine/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,127 @@ impl Engine {
widget_flags
}

/// Insert an SVG image as a VectorImage stroke.
/// If `typst_source` is provided, it will be stored with the image for later editing.
pub fn insert_svg_image(
&mut self,
svg_data: String,
pos: na::Vector2<f64>,
typst_source: Option<String>,
) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();

// Deselect all strokes
let all_strokes = self.store.stroke_keys_as_rendered();
self.store.set_selected_keys(&all_strokes, false);

// Create VectorImage from SVG
match VectorImage::from_svg_str(&svg_data, pos, ImageSizeOption::RespectOriginalSize) {
Ok(mut vectorimage) => {
// Store the Typst source if provided
vectorimage.typst_source = typst_source;

let stroke = Stroke::VectorImage(vectorimage);
let stroke_key = self.store.insert_stroke(stroke, None);

self.store.regenerate_rendering_for_stroke(
stroke_key,
self.camera.viewport(),
self.camera.image_scale(),
);

widget_flags |= self.store.record(Instant::now());
widget_flags.resize = true;
widget_flags.redraw = true;
widget_flags.store_modified = true;
}
Err(e) => {
error!("Failed to import SVG image: {e:?}");
}
}

widget_flags
}

/// Update an existing Typst stroke with new SVG data and source code.
pub fn update_typst_stroke(
&mut self,
stroke_key: StrokeKey,
svg_data: String,
typst_source: String,
) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();

if let Some(Stroke::VectorImage(vectorimage)) = self.store.get_stroke_mut(stroke_key) {
// Store the old intrinsic size, cuboid size, and transform
let old_cuboid_size = vectorimage.rectangle.cuboid.half_extents * 2.0;
let old_transform = vectorimage.rectangle.transform;

// Create new VectorImage to get the new intrinsic size
match VectorImage::from_svg_str(
&svg_data,
na::Vector2::zeros(),
ImageSizeOption::RespectOriginalSize,
) {
Ok(mut new_vectorimage) => {
let new_intrinsic_size = new_vectorimage.intrinsic_size;

// Calculate width scale factor based on old cuboid width vs new intrinsic width
let width_scale_factor = if new_intrinsic_size[0] > 0.0 {
old_cuboid_size[0] / new_intrinsic_size[0]
} else {
1.0
};

// Apply the scale factor to both dimensions to maintain aspect ratio
let new_cuboid_size = na::vector![
new_intrinsic_size[0] * width_scale_factor,
new_intrinsic_size[1] * width_scale_factor
];

// Calculate height difference to adjust position
let height_diff = new_cuboid_size[1] - old_cuboid_size[1];

// Set the new cuboid size
new_vectorimage.rectangle.cuboid =
p2d::shape::Cuboid::new(new_cuboid_size * 0.5);

// Restore the original transform and adjust for height change
new_vectorimage.rectangle.transform = old_transform;
// Adjust y position to make it look like text was written further
new_vectorimage
.rectangle
.transform
.append_translation_mut(na::vector![0.0, height_diff * 0.5]);

// Store the Typst source
new_vectorimage.typst_source = Some(typst_source);

// Update the stroke
*vectorimage = new_vectorimage;

self.store.regenerate_rendering_for_stroke(
stroke_key,
self.camera.viewport(),
self.camera.image_scale(),
);

widget_flags |= self.store.record(Instant::now());
widget_flags.resize = true;
widget_flags.redraw = true;
widget_flags.store_modified = true;
}
Err(e) => {
error!("Failed to update Typst stroke: {e:?}");
}
}
} else {
error!("Failed to update Typst stroke: stroke not found or not a VectorImage");
}

widget_flags
}

/// Insert the stroke content.
///
/// The data usually comes from the clipboard, drag-and-drop, ..
Expand Down
11 changes: 11 additions & 0 deletions crates/rnote-engine/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub struct EngineViewMut<'a> {
pub camera: &'a mut Camera,
pub audioplayer: &'a mut Option<AudioPlayer>,
pub animation: &'a mut Animation,
pub clicked_typst_stroke: &'a mut Option<(StrokeKey, String)>,
}

/// Constructs an `EngineViewMut` from an identifier containing an `Engine` instance.
Expand All @@ -88,6 +89,7 @@ macro_rules! engine_view_mut {
($engine:ident) => {
$crate::engine::EngineViewMut {
tasks_tx: $engine.tasks_tx.clone(),
clicked_typst_stroke: &mut $engine.clicked_typst_stroke,
config: &mut $engine.config.write(),
document: &mut $engine.document,
store: &mut $engine.store,
Expand Down Expand Up @@ -206,6 +208,9 @@ pub struct Engine {
#[cfg(feature = "ui")]
#[serde(skip)]
origin_indicator_rendernode: Option<gtk4::gsk::RenderNode>,
// Clicked Typst stroke for editing
#[serde(skip)]
clicked_typst_stroke: Option<(StrokeKey, String)>,
}

impl Default for Engine {
Expand All @@ -229,6 +234,7 @@ impl Default for Engine {
origin_indicator_image: None,
#[cfg(feature = "ui")]
origin_indicator_rendernode: None,
clicked_typst_stroke: None,
}
}
}
Expand Down Expand Up @@ -265,6 +271,11 @@ impl Engine {
self.tasks_rx.take()
}

/// Takes the clicked Typst stroke if any, clearing it from the engine.
pub fn take_clicked_typst_stroke(&mut self) -> Option<(StrokeKey, String)> {
self.clicked_typst_stroke.take()
}

/// Whether pen sounds are enabled.
pub fn pen_sounds(&self) -> bool {
self.config.read().pen_sounds
Expand Down
2 changes: 2 additions & 0 deletions crates/rnote-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub mod snap;
pub mod store;
pub mod strokes;
pub mod tasks;
#[cfg(feature = "ui")]
pub mod typst;
pub mod utils;
pub mod widgetflags;

Expand Down
Loading