Skip to content

Commit 38e2c61

Browse files
committed
Edit Typst source (supports rotation and scaling) and store it in the
files
1 parent a99b8d4 commit 38e2c61

File tree

7 files changed

+183
-14
lines changed

7 files changed

+183
-14
lines changed

crates/rnote-engine/src/engine/import.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,10 +363,12 @@ impl Engine {
363363
}
364364

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

@@ -376,7 +378,10 @@ impl Engine {
376378

377379
// Create VectorImage from SVG
378380
match VectorImage::from_svg_str(&svg_data, pos, ImageSizeOption::RespectOriginalSize) {
379-
Ok(vectorimage) => {
381+
Ok(mut vectorimage) => {
382+
// Store the Typst source if provided
383+
vectorimage.typst_source = typst_source;
384+
380385
let stroke = Stroke::VectorImage(vectorimage);
381386
let stroke_key = self.store.insert_stroke(stroke, None);
382387

@@ -399,6 +404,81 @@ impl Engine {
399404
widget_flags
400405
}
401406

407+
/// Update an existing Typst stroke with new SVG data and source code.
408+
pub fn update_typst_stroke(
409+
&mut self,
410+
stroke_key: StrokeKey,
411+
svg_data: String,
412+
typst_source: String,
413+
) -> WidgetFlags {
414+
let mut widget_flags = WidgetFlags::default();
415+
416+
if let Some(Stroke::VectorImage(vectorimage)) = self.store.get_stroke_mut(stroke_key) {
417+
// Store the old intrinsic size, cuboid size, and transform
418+
let old_cuboid_size = vectorimage.rectangle.cuboid.half_extents * 2.0;
419+
let old_transform = vectorimage.rectangle.transform.clone();
420+
421+
// Create new VectorImage to get the new intrinsic size
422+
match VectorImage::from_svg_str(
423+
&svg_data,
424+
na::Vector2::zeros(),
425+
ImageSizeOption::RespectOriginalSize,
426+
) {
427+
Ok(mut new_vectorimage) => {
428+
let new_intrinsic_size = new_vectorimage.intrinsic_size;
429+
430+
// Calculate width scale factor based on old cuboid width vs new intrinsic width
431+
let width_scale_factor = if new_intrinsic_size[0] > 0.0 {
432+
old_cuboid_size[0] / new_intrinsic_size[0]
433+
} else {
434+
1.0
435+
};
436+
437+
// Apply the scale factor to both dimensions to maintain aspect ratio
438+
let new_cuboid_size = na::vector![
439+
new_intrinsic_size[0] * width_scale_factor,
440+
new_intrinsic_size[1] * width_scale_factor
441+
];
442+
443+
// Calculate height difference to adjust position
444+
let height_diff = new_cuboid_size[1] - old_cuboid_size[1];
445+
446+
// Set the new cuboid size
447+
new_vectorimage.rectangle.cuboid = p2d::shape::Cuboid::new(new_cuboid_size * 0.5);
448+
449+
// Restore the original transform and adjust for height change
450+
new_vectorimage.rectangle.transform = old_transform;
451+
// Adjust y position to make it look like text was written further
452+
new_vectorimage.rectangle.transform.append_translation_mut(na::vector![0.0, height_diff * 0.5]);
453+
454+
// Store the Typst source
455+
new_vectorimage.typst_source = Some(typst_source);
456+
457+
// Update the stroke
458+
*vectorimage = new_vectorimage;
459+
460+
self.store.regenerate_rendering_for_stroke(
461+
stroke_key,
462+
self.camera.viewport(),
463+
self.camera.image_scale(),
464+
);
465+
466+
widget_flags |= self.store.record(Instant::now());
467+
widget_flags.resize = true;
468+
widget_flags.redraw = true;
469+
widget_flags.store_modified = true;
470+
}
471+
Err(e) => {
472+
error!("Failed to update Typst stroke: {e:?}");
473+
}
474+
}
475+
} else {
476+
error!("Failed to update Typst stroke: stroke not found or not a VectorImage");
477+
}
478+
479+
widget_flags
480+
}
481+
402482
/// Insert the stroke content.
403483
///
404484
/// The data usually comes from the clipboard, drag-and-drop, ..

crates/rnote-engine/src/engine/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub struct EngineViewMut<'a> {
8080
pub camera: &'a mut Camera,
8181
pub audioplayer: &'a mut Option<AudioPlayer>,
8282
pub animation: &'a mut Animation,
83+
pub clicked_typst_stroke: &'a mut Option<(StrokeKey, String)>,
8384
}
8485

8586
/// Constructs an `EngineViewMut` from an identifier containing an `Engine` instance.
@@ -88,6 +89,7 @@ macro_rules! engine_view_mut {
8889
($engine:ident) => {
8990
$crate::engine::EngineViewMut {
9091
tasks_tx: $engine.tasks_tx.clone(),
92+
clicked_typst_stroke: &mut $engine.clicked_typst_stroke,
9193
config: &mut $engine.config.write(),
9294
document: &mut $engine.document,
9395
store: &mut $engine.store,
@@ -206,6 +208,9 @@ pub struct Engine {
206208
#[cfg(feature = "ui")]
207209
#[serde(skip)]
208210
origin_indicator_rendernode: Option<gtk4::gsk::RenderNode>,
211+
// Clicked Typst stroke for editing
212+
#[serde(skip)]
213+
clicked_typst_stroke: Option<(StrokeKey, String)>,
209214
}
210215

211216
impl Default for Engine {
@@ -229,6 +234,7 @@ impl Default for Engine {
229234
origin_indicator_image: None,
230235
#[cfg(feature = "ui")]
231236
origin_indicator_rendernode: None,
237+
clicked_typst_stroke: None,
232238
}
233239
}
234240
}
@@ -265,6 +271,11 @@ impl Engine {
265271
self.tasks_rx.take()
266272
}
267273

274+
/// Takes the clicked Typst stroke if any, clearing it from the engine.
275+
pub fn take_clicked_typst_stroke(&mut self) -> Option<(StrokeKey, String)> {
276+
self.clicked_typst_stroke.take()
277+
}
278+
268279
/// Whether pen sounds are enabled.
269280
pub fn pen_sounds(&self) -> bool {
270281
self.config.read().pen_sounds

crates/rnote-engine/src/pens/typewriter/penevents.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,19 @@ impl Typewriter {
4343
.stroke_hitboxes_contain_coord(engine_view.camera.viewport(), element.pos)
4444
.last()
4545
{
46+
// Check if clicked on a Typst VectorImage
47+
if let Some(Stroke::VectorImage(vectorimage)) =
48+
engine_view.store.get_stroke_ref(stroke_key)
49+
{
50+
if let Some(ref typst_source) = vectorimage.typst_source {
51+
// Signal to UI that a Typst element was clicked for editing
52+
*engine_view.clicked_typst_stroke =
53+
Some((stroke_key, typst_source.clone()));
54+
// Don't change state, just let UI handle opening the dialog
55+
}
56+
}
4657
// When clicked on a textstroke, we start modifying it
47-
if let Some(Stroke::TextStroke(textstroke)) =
58+
else if let Some(Stroke::TextStroke(textstroke)) =
4859
engine_view.store.get_stroke_ref(stroke_key)
4960
{
5061
let cursor = if let Ok(new_cursor) =

crates/rnote-engine/src/strokes/vectorimage.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ pub struct VectorImage {
3131
pub intrinsic_size: na::Vector2<f64>,
3232
#[serde(rename = "rectangle")]
3333
pub rectangle: Rectangle,
34+
/// Optional Typst source code, if this image was generated from Typst
35+
#[serde(
36+
rename = "typst_source",
37+
skip_serializing_if = "Option::is_none",
38+
default
39+
)]
40+
pub typst_source: Option<String>,
3441
}
3542

3643
impl Default for VectorImage {
@@ -39,6 +46,7 @@ impl Default for VectorImage {
3946
svg_data: String::default(),
4047
intrinsic_size: na::Vector2::zeros(),
4148
rectangle: Rectangle::default(),
49+
typst_source: None,
4250
}
4351
}
4452
}
@@ -201,6 +209,7 @@ impl VectorImage {
201209
svg_data,
202210
intrinsic_size,
203211
rectangle,
212+
typst_source: None,
204213
})
205214
}
206215

crates/rnote-ui/src/canvas/input.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Imports
22
use super::RnCanvas;
3+
use glib::clone;
34
use gtk4::{Native, gdk, glib, graphene, prelude::*};
45
use rnote_compose::penevent::{KeyboardKey, ModifierKey, PenEvent, PenState, ShortcutKey};
56
use rnote_compose::penpath::Element;
@@ -192,6 +193,33 @@ pub(crate) fn handle_pointer_controller_event(
192193
);
193194
widget_flags |= wf;
194195
propagation = ep.into_glib();
196+
197+
// Check if a Typst element was clicked for editing (only on Up to avoid multiple triggers)
198+
if let Some((stroke_key, typst_source)) =
199+
canvas.engine_mut().take_clicked_typst_stroke()
200+
{
201+
// Get the appwindow from the canvas root
202+
if let Some(root) = canvas.root() {
203+
if let Ok(appwindow) = root.downcast::<crate::RnAppWindow>() {
204+
// Open the Typst editor dialog with the existing source
205+
glib::spawn_future_local(clone!(
206+
#[weak]
207+
canvas,
208+
#[weak]
209+
appwindow,
210+
async move {
211+
crate::dialogs::typsteditor::dialog_typst_editor(
212+
&appwindow,
213+
&canvas,
214+
Some(typst_source),
215+
Some(stroke_key),
216+
)
217+
.await;
218+
}
219+
));
220+
}
221+
}
222+
}
195223
}
196224
PenState::Proximity => {
197225
canvas.enable_drawing_cursor(false);
@@ -227,6 +255,7 @@ pub(crate) fn handle_pointer_controller_event(
227255
}
228256

229257
canvas.emit_handle_widget_flags(widget_flags);
258+
230259
(propagation, pen_state)
231260
}
232261

crates/rnote-ui/src/dialogs/typsteditor.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ use std::rc::Rc;
99
use std::time::Duration;
1010
use tracing::{error, warn};
1111

12-
pub(crate) async fn dialog_typst_editor(appwindow: &RnAppWindow, canvas: &RnCanvas) {
12+
pub(crate) async fn dialog_typst_editor(
13+
appwindow: &RnAppWindow,
14+
canvas: &RnCanvas,
15+
initial_source: Option<String>,
16+
editing_stroke_key: Option<rnote_engine::store::StrokeKey>,
17+
) {
1318
let builder = Builder::from_resource(
1419
(String::from(crate::config::APP_IDPATH) + "ui/dialogs/typsteditor.ui").as_str(),
1520
);
@@ -44,8 +49,12 @@ pub(crate) async fn dialog_typst_editor(appwindow: &RnAppWindow, canvas: &RnCanv
4449
let text_buffer = textview_source.buffer();
4550
let error_buffer = textview_error.buffer();
4651

47-
// Set default Typst content
48-
text_buffer.set_text("#set page(width: auto, height: auto, margin: 2pt)\n\n= Hello Typst!\n\nThis is a *bold* text and _italic_ text.\n\n$ sum_(i=1)^n i = (n(n+1))/2 $");
52+
// Set Typst content (either provided or default)
53+
if let Some(source) = initial_source {
54+
text_buffer.set_text(&source);
55+
} else {
56+
text_buffer.set_text("#set page(width: auto, height: auto, margin: 2pt)\n\n= Hello Typst!\n\nThis is a *bold* text and _italic_ text.\n\n$ sum_(i=1)^n i = (n(n+1))/2 $");
57+
}
4958

5059
// Shared state for compiled SVG and debounce timer
5160
let compiled_svg: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
@@ -166,16 +175,31 @@ pub(crate) async fn dialog_typst_editor(appwindow: &RnAppWindow, canvas: &RnCanv
166175
canvas,
167176
#[weak]
168177
appwindow,
178+
#[weak]
179+
text_buffer,
169180
#[strong]
170181
compiled_svg,
171182
move |_| {
172183
if let Some(svg) = compiled_svg.borrow().as_ref() {
173-
// Get the center of the current viewport as insertion position
174-
let viewport = canvas.engine_ref().camera.viewport();
175-
let pos = na::vector![viewport.center().x, viewport.center().y];
176-
177-
// Insert the SVG into the canvas
178-
let widget_flags = canvas.engine_mut().insert_svg_image(svg.clone(), pos);
184+
// Get the Typst source code
185+
let source =
186+
text_buffer.text(&text_buffer.start_iter(), &text_buffer.end_iter(), false);
187+
188+
let widget_flags = if let Some(stroke_key) = editing_stroke_key {
189+
// Update existing stroke
190+
canvas.engine_mut().update_typst_stroke(
191+
stroke_key,
192+
svg.clone(),
193+
source.to_string(),
194+
)
195+
} else {
196+
// Insert new stroke at center of viewport
197+
let viewport = canvas.engine_ref().camera.viewport();
198+
let pos = na::vector![viewport.center().x, viewport.center().y];
199+
canvas
200+
.engine_mut()
201+
.insert_svg_image(svg.clone(), pos, Some(source.to_string()))
202+
};
179203
appwindow.handle_widget_flags(widget_flags, &canvas);
180204

181205
dialog.close();

crates/rnote-ui/src/penssidebar/typewriterpage.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,15 @@ impl RnTypewriterPage {
199199
return;
200200
};
201201
glib::spawn_future_local(clone!(
202-
#[weak] appwindow,
203-
#[weak] canvas,
202+
#[weak]
203+
appwindow,
204+
#[weak]
205+
canvas,
204206
async move {
205-
crate::dialogs::typsteditor::dialog_typst_editor(&appwindow, &canvas).await;
207+
crate::dialogs::typsteditor::dialog_typst_editor(
208+
&appwindow, &canvas, None, None,
209+
)
210+
.await;
206211
}
207212
));
208213
}

0 commit comments

Comments
 (0)