Skip to content

Commit 4b1ef0c

Browse files
committed
feat: spellcheck correction picker
1 parent 96dce63 commit 4b1ef0c

File tree

11 files changed

+364
-15
lines changed

11 files changed

+364
-15
lines changed

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

+35
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,41 @@ impl Engine {
379379
widget_flags
380380
}
381381

382+
pub fn get_spellcheck_corrections(&self) -> Option<Vec<String>> {
383+
if let Pen::Typewriter(typewriter) = self.penholder.current_pen_ref() {
384+
return typewriter.get_spellcheck_correction_in_modifying_stroke(&mut EngineView {
385+
tasks_tx: self.tasks_tx.clone(),
386+
pens_config: &self.pens_config,
387+
document: &self.document,
388+
store: &self.store,
389+
camera: &self.camera,
390+
audioplayer: &self.audioplayer,
391+
spellcheck: &self.spellcheck,
392+
});
393+
}
394+
395+
None
396+
}
397+
398+
pub fn apply_spellcheck_correction(&mut self, correction: &str) -> WidgetFlags {
399+
if let Pen::Typewriter(typewriter) = self.penholder.current_pen_mut() {
400+
return typewriter.apply_spellcheck_correction_in_modifying_stroke(
401+
correction,
402+
&mut EngineViewMut {
403+
tasks_tx: self.tasks_tx.clone(),
404+
pens_config: &mut self.pens_config,
405+
document: &mut self.document,
406+
store: &mut self.store,
407+
camera: &mut self.camera,
408+
audioplayer: &mut self.audioplayer,
409+
spellcheck: &mut self.spellcheck,
410+
},
411+
);
412+
}
413+
414+
WidgetFlags::default()
415+
}
416+
382417
pub fn optimize_epd(&self) -> bool {
383418
self.optimize_epd
384419
}

crates/rnote-engine/src/pens/penholder.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ impl PenHolder {
140140
self.progress
141141
}
142142

143-
pub fn current_pen_ref(&mut self) -> &Pen {
143+
pub fn current_pen_ref(&self) -> &Pen {
144144
&self.current_pen
145145
}
146146

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

+53
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,59 @@ impl Typewriter {
827827
}
828828
}
829829

830+
pub(crate) fn get_spellcheck_correction_in_modifying_stroke(
831+
&self,
832+
engine_view: &EngineView,
833+
) -> Option<Vec<String>> {
834+
if let TypewriterState::Modifying {
835+
stroke_key, cursor, ..
836+
} = &self.state
837+
{
838+
if let Some(Stroke::TextStroke(textstroke)) =
839+
engine_view.store.get_stroke_ref(*stroke_key)
840+
{
841+
return textstroke.get_spellcheck_corrections_at_index(
842+
engine_view.spellcheck,
843+
cursor.cur_cursor(),
844+
);
845+
}
846+
}
847+
848+
None
849+
}
850+
851+
pub(crate) fn apply_spellcheck_correction_in_modifying_stroke(
852+
&mut self,
853+
correction: &str,
854+
engine_view: &mut EngineViewMut,
855+
) -> WidgetFlags {
856+
let mut widget_flags = WidgetFlags::default();
857+
858+
if let TypewriterState::Modifying {
859+
stroke_key, cursor, ..
860+
} = &mut self.state
861+
{
862+
if let Some(Stroke::TextStroke(textstroke)) =
863+
engine_view.store.get_stroke_mut(*stroke_key)
864+
{
865+
textstroke.apply_spellcheck_correction_at_cursor(cursor, correction);
866+
867+
engine_view.store.update_geometry_for_stroke(*stroke_key);
868+
engine_view.store.regenerate_rendering_for_stroke(
869+
*stroke_key,
870+
engine_view.camera.viewport(),
871+
engine_view.camera.image_scale(),
872+
);
873+
874+
widget_flags |= engine_view.store.record(Instant::now());
875+
widget_flags.redraw = true;
876+
widget_flags.store_modified = true;
877+
}
878+
}
879+
880+
widget_flags
881+
}
882+
830883
pub(crate) fn toggle_text_attribute_current_selection(
831884
&mut self,
832885
text_attribute: TextAttribute,

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

+52-2
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ impl TextStyle {
415415
camera: &Camera,
416416
) {
417417
const ERROR_COLOR: piet::Color = color::GNOME_REDS[2];
418+
const STYLE: piet::StrokeStyle = piet::StrokeStyle::new().line_cap(piet::LineCap::Round);
419+
418420
let scale = 1.0 / camera.total_zoom();
419421

420422
if let Ok(selection_rects) =
@@ -433,7 +435,7 @@ impl TextStyle {
433435
);
434436

435437
let path = create_wavy_line(origin, width, scale);
436-
cx.stroke(path, &ERROR_COLOR, 1.5 * scale);
438+
cx.stroke_styled(path, &ERROR_COLOR, 1.5 * scale, &STYLE);
437439
}
438440
}
439441
}
@@ -685,6 +687,52 @@ impl TextStroke {
685687
}
686688
}
687689

690+
pub fn get_spellcheck_corrections_at_index(
691+
&self,
692+
spellcheck: &Spellcheck,
693+
index: usize,
694+
) -> Option<Vec<String>> {
695+
let Some(dict) = &spellcheck.dict else {
696+
return None;
697+
};
698+
699+
let start_index = self.get_prev_word_start_index(index);
700+
701+
if let Some(length) = self.spellcheck_result.errors.get(&start_index) {
702+
let word = self.get_text_slice_for_range(start_index..start_index + length);
703+
return Some(dict.suggest(word));
704+
}
705+
706+
None
707+
}
708+
709+
pub fn apply_spellcheck_correction_at_cursor(
710+
&mut self,
711+
cursor: &mut GraphemeCursor,
712+
correction: &str,
713+
) {
714+
let cur_pos = cursor.cur_cursor();
715+
let start_index = self.get_prev_word_start_index(cur_pos);
716+
717+
if let Some(length) = self.spellcheck_result.errors.get(&start_index) {
718+
let old_length = *length;
719+
let new_length = correction.len();
720+
721+
self.text
722+
.replace_range(start_index..start_index + old_length, correction);
723+
724+
self.spellcheck_result.errors.remove(&start_index);
725+
726+
// translate the text attributes
727+
self.translate_attrs_after_cursor(
728+
start_index + old_length,
729+
(new_length as i32) - (old_length as i32),
730+
);
731+
732+
*cursor = GraphemeCursor::new(start_index + new_length, self.text.len(), true);
733+
}
734+
}
735+
688736
pub fn check_spelling_range(
689737
&mut self,
690738
start_index: usize,
@@ -870,7 +918,9 @@ impl TextStroke {
870918
};
871919

872920
for (word_start, word_length) in translated_words {
873-
let new_word_start = word_start.saturating_add_signed(offset as isize);
921+
let Some(new_word_start) = word_start.checked_add_signed(offset as isize) else {
922+
continue;
923+
};
874924

875925
if new_word_start >= from_pos {
876926
self.spellcheck_result
Loading

crates/rnote-ui/data/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ rnote_ui_gresources_icons_files = files(
275275
'icons/scalable/actions/text-indent-less-symbolic.svg',
276276
'icons/scalable/actions/text-indent-more-symbolic.svg',
277277
'icons/scalable/actions/text-italic-symbolic.svg',
278+
'icons/scalable/actions/text-squiggly-symbolic.svg',
278279
'icons/scalable/actions/text-strikethrough-symbolic.svg',
279280
'icons/scalable/actions/text-underline-symbolic.svg',
280281
'icons/scalable/actions/touch-two-finger-long-press-symbolic.svg',

crates/rnote-ui/data/resources.gresource.xml

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
<file compressed="true">icons/scalable/actions/text-indent-less-symbolic.svg</file>
147147
<file compressed="true">icons/scalable/actions/text-indent-more-symbolic.svg</file>
148148
<file compressed="true">icons/scalable/actions/text-italic-symbolic.svg</file>
149+
<file compressed="true">icons/scalable/actions/text-squiggly-symbolic.svg</file>
149150
<file compressed="true">icons/scalable/actions/text-strikethrough-symbolic.svg</file>
150151
<file compressed="true">icons/scalable/actions/text-underline-symbolic.svg</file>
151152
<file compressed="true">icons/scalable/actions/touch-two-finger-long-press-symbolic.svg</file>

crates/rnote-ui/data/ui/penssidebar/typewriterpage.ui

+93
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,99 @@
5151
<object class="GtkEmojiChooser" id="emojichooser">
5252
<property name="position">right</property>
5353
</object>
54+
<child>
55+
<object class="GtkMenuButton" id="spellcheck_corrections_menubutton">
56+
<property name="direction">left</property>
57+
<property name="tooltip_text" translatable="yes">Spellcheck Corrections</property>
58+
<property name="icon-name">text-squiggly-symbolic</property>
59+
<style>
60+
<class name="flat" />
61+
<class name="sidebar_action_button" />
62+
</style>
63+
</object>
64+
</child>
65+
<object class="GtkPopover" id="spellcheck_corrections">
66+
<child>
67+
<object class="GtkListView" id="spellcheck_corrections_listview">
68+
<property name="single-click-activate">true</property>
69+
<style>
70+
<class name="navigation-sidebar" />
71+
</style>
72+
</object>
73+
</child>
74+
</object>
75+
<object class="GtkPopover" id="spellcheck_corrections_unavailable">
76+
<child>
77+
<object class="GtkBox">
78+
<property name="orientation">vertical</property>
79+
<property name="spacing">12</property>
80+
<property name="margin-start">18</property>
81+
<property name="margin-end">18</property>
82+
<property name="margin-top">18</property>
83+
<property name="margin-bottom">18</property>
84+
<child>
85+
<object class="GtkImage">
86+
<property name="icon-name">text-squiggly-symbolic</property>
87+
<property name="pixel-size">64</property>
88+
<style>
89+
<class name="dim-label"/>
90+
</style>
91+
</object>
92+
</child>
93+
<child>
94+
<object class="GtkLabel">
95+
<property name="justify">center</property>
96+
<property name="label" translatable="yes">No Corrections
97+
Available</property>
98+
<style>
99+
<class name="title-4"/>
100+
</style>
101+
</object>
102+
</child>
103+
<child>
104+
<object class="GtkLabel">
105+
<property name="justify">center</property>
106+
<property name="label" translatable="yes">Move cursor over underlined
107+
words to get suggestions.</property>
108+
<style>
109+
<class name="dim-label"/>
110+
</style>
111+
</object>
112+
</child>
113+
</object>
114+
</child>
115+
</object>
116+
<object class="GtkPopover" id="spellcheck_corrections_empty">
117+
<child>
118+
<object class="GtkBox">
119+
<property name="orientation">vertical</property>
120+
<property name="spacing">12</property>
121+
<property name="margin-start">18</property>
122+
<property name="margin-end">18</property>
123+
<property name="margin-top">18</property>
124+
<property name="margin-bottom">18</property>
125+
<child>
126+
<object class="GtkImage">
127+
<property name="icon-name">text-squiggly-symbolic</property>
128+
<property name="pixel-size">64</property>
129+
<style>
130+
<class name="dim-label"/>
131+
</style>
132+
</object>
133+
</child>
134+
<child>
135+
<object class="GtkLabel">
136+
<property name="justify">center</property>
137+
<property name="label" translatable="yes">No Corrections
138+
Found</property>
139+
<style>
140+
<class name="title-4"/>
141+
</style>
142+
</object>
143+
</child>
144+
</object>
145+
</child>
146+
</object>
54147
<child>
55148
<object class="GtkSeparator">
56149
<property name="orientation">vertical</property>

crates/rnote-ui/src/appwindow/imp.rs

+10
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,11 @@ impl RnAppWindow {
635635
.typewriter_page()
636636
.emojichooser_menubutton()
637637
.set_direction(ArrowType::Right);
638+
obj.overlays()
639+
.penssidebar()
640+
.typewriter_page()
641+
.spellcheck_corrections_menubutton()
642+
.set_direction(ArrowType::Right);
638643
obj.overlays()
639644
.penssidebar()
640645
.eraser_page()
@@ -762,6 +767,11 @@ impl RnAppWindow {
762767
.typewriter_page()
763768
.emojichooser_menubutton()
764769
.set_direction(ArrowType::Left);
770+
obj.overlays()
771+
.penssidebar()
772+
.typewriter_page()
773+
.spellcheck_corrections_menubutton()
774+
.set_direction(ArrowType::Left);
765775
obj.overlays()
766776
.penssidebar()
767777
.eraser_page()

0 commit comments

Comments
 (0)