From d304ad9252bf2c04724d7e0e4bc7ee9e3bd9a1e6 Mon Sep 17 00:00:00 2001 From: Olaf Wintermann Date: Fri, 17 Jul 2026 09:00:35 +0200 Subject: [PATCH] replace NoteDoc enum with NoteViewModel trait --- application/note/src/file.rs | 17 +++- application/note/src/note.rs | 161 ++++++++++++++++--------------- application/note/src/notebook.rs | 73 +++----------- ui-rs/src/ui/event.rs | 15 --- ui-rs/src/ui/toolkit.rs | 104 +++++++++++--------- 5 files changed, 167 insertions(+), 203 deletions(-) diff --git a/application/note/src/file.rs b/application/note/src/file.rs index 560c2c1..3a9df0f 100644 --- a/application/note/src/file.rs +++ b/application/note/src/file.rs @@ -25,11 +25,13 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ + use std::rc::Rc; use backend::backend::{BackendHandle, NoteId, OpenExternResult}; -use ui_rs::{action, ui_actions, UiModel}; +use ui_rs::{action, doc_cast, ui_actions, UiModel}; use ui_rs::ui::*; use crate::AppStates; +use crate::note::NoteViewModel; use crate::window::NoteTypeTabView; #[derive(UiModel)] @@ -73,14 +75,15 @@ impl FileNote { } } - pub fn into_doc(self, model: &entity::note::Model) -> UiDoc { - UiDoc::new2(self, |n,d| { + pub fn into_doc(self, model: &entity::note::Model) -> UiDoc { + let doc: UiDoc = UiDoc::new_from_box2(Box::new(self), |n,d| { n.doc = d.doc_ref(); n.nodename = model.nodename.clone(); n.note_type.set(NoteTypeTabView::File as i64); n.note_nodename.set(model.nodename.as_str()); - }) + }); + doc_cast!(doc, dyn NoteViewModel) } #[action] @@ -146,3 +149,9 @@ impl FileNote { doc.ctx.set_state(AppStates::NoteMaximized as i32); } } + +impl NoteViewModel for FileNote { + fn save_note(&mut self) { + + } +} \ No newline at end of file diff --git a/application/note/src/note.rs b/application/note/src/note.rs index 6307b1e..025fd10 100644 --- a/application/note/src/note.rs +++ b/application/note/src/note.rs @@ -33,13 +33,18 @@ use sea_orm::sea_query::prelude::Utc; use backend::backend::{BackendHandle, BroadcastMessage, NoteId, NoteTitleUpdate, SaveNoteResult}; use backend::lockmanager::NoteLock; use entity::note::NoteType; -use ui_rs::{action, ui_actions, UiModel}; +use ui_rs::{action, doc_cast, ui_actions, UiModel}; use ui_rs::ui::*; use crate::AppStates; use crate::window::NoteTypeTabView; static TMP_ID: AtomicU64 = AtomicU64::new(1); +pub trait NoteViewModel: UiModel + UiActions { + fn save_note(&mut self); +} + + #[derive(UiModel)] pub struct Note { pub doc: UiDocRef, @@ -98,8 +103,8 @@ impl Note { } } - pub fn into_doc(self, model: &entity::note::Model) -> UiDoc { - UiDoc::new2(self, |n,d| { + pub fn into_doc(self, model: &entity::note::Model) -> UiDoc { + let doc: UiDoc = UiDoc::new_from_box2(Box::new(self), |n,d| { n.doc = d.doc_ref(); if n.id.is_new() { n.init_new(); @@ -110,9 +115,11 @@ impl Note { } // make sure we receive an event, when this note is attached or detached d.ctx.on_attachment_status_change_action("attachment_status_changed"); - }) + }); + doc_cast!(doc, dyn NoteViewModel) } + /// Handles any attachment status change, which includes if this note is /// directly attached or detached, but also if any parent attachment status changes #[action] @@ -225,78 +232,6 @@ impl Note { self.save_note(); } - pub fn save_note(&mut self) { - if !self.modified { - return; - } - - let Some(doc) = self.doc.get_doc() else { - return; - }; - - let content = self.text.get(); - let title = match generate_title(content.as_str()) { - Some(title) => title.0.to_string(), - None => "New Note".to_string() // TODO: see NoteItem::new() - }; - - let (note_id, created) = match &self.id { - NoteId::Id(id) => (Set(*id), NotSet), - NoteId::TmpId(_) => (NotSet, Set(Utc::now().into())) - }; - - let note = entity::note::ActiveModel { - note_id: note_id.clone(), - collection_id: Set(self.collection_id), - nodename: if let Some(nodename) = &self.nodename { Set(nodename.clone())} else { NotSet }, - kind: Set(self.kind.clone()), - title: Set(title), - lastmodified: Set(Utc::now().into()), - created: created, - version: Set(self.version) - }; - - let notecontent = entity::notecontent::ActiveModel { - id: if self.content_id == 0 { NotSet } else { Set(self.content_id) }, - note_id: note_id, - content: Set(content), - }; - - let proxy = doc.doc_proxy(); - self.backend.save_note(self.notebook_instance_id, self.id.clone(), note, Some(notecontent), |result|{ - proxy.call_mainthread(move |doc, note|{ - match result { - SaveNoteResult::Ok((notemodel, content_id)) => { - note.id = NoteId::Id(notemodel.note_id); - note.content_id = content_id; - note.version = notemodel.version; - println!("Note saved: note_id: {}, content_id: {}, version: {}", notemodel.note_id, content_id, note.version); - - // make sure the "new note" button is always enabled within this note - doc.ctx.unsuppress_state(AppStates::NoteEnableNew as i32); - }, - SaveNoteResult::VersionConflict => { - println!("Failed to save note: version conflict"); - }, - SaveNoteResult::Error(e) => { - println!("Failed to save note: {}", e); - }, - SaveNoteResult::ValidationError(s) => { - println!("Failed to save note: validation error: {}", s); - } - SaveNoteResult::FileAlreadyExists(path) => { - println!("Failed to save note: file already exists: {}", path.display()); - } - SaveNoteResult::FileError(e) => { - println!("Failed to save note: file error: {}", e); - } - } - }); - }); - - self.modified = false; - } - #[action(u64)] pub fn changed_by(&mut self, _event: &ActionEvent, _from: &u64) { // The from id is currently unused, however it may be important when it is possible @@ -380,6 +315,80 @@ impl Note { } } +impl NoteViewModel for Note { + fn save_note(&mut self) { + if !self.modified { + return; + } + + let Some(doc) = self.doc.get_doc() else { + return; + }; + + let content = self.text.get(); + let title = match generate_title(content.as_str()) { + Some(title) => title.0.to_string(), + None => "New Note".to_string() // TODO: see NoteItem::new() + }; + + let (note_id, created) = match &self.id { + NoteId::Id(id) => (Set(*id), NotSet), + NoteId::TmpId(_) => (NotSet, Set(Utc::now().into())) + }; + + let note = entity::note::ActiveModel { + note_id: note_id.clone(), + collection_id: Set(self.collection_id), + nodename: if let Some(nodename) = &self.nodename { Set(nodename.clone())} else { NotSet }, + kind: Set(self.kind.clone()), + title: Set(title), + lastmodified: Set(Utc::now().into()), + created: created, + version: Set(self.version) + }; + + let notecontent = entity::notecontent::ActiveModel { + id: if self.content_id == 0 { NotSet } else { Set(self.content_id) }, + note_id: note_id, + content: Set(content), + }; + + let proxy = doc.doc_proxy(); + self.backend.save_note(self.notebook_instance_id, self.id.clone(), note, Some(notecontent), |result|{ + proxy.call_mainthread(move |doc, note|{ + match result { + SaveNoteResult::Ok((notemodel, content_id)) => { + note.id = NoteId::Id(notemodel.note_id); + note.content_id = content_id; + note.version = notemodel.version; + println!("Note saved: note_id: {}, content_id: {}, version: {}", notemodel.note_id, content_id, note.version); + + // make sure the "new note" button is always enabled within this note + doc.ctx.unsuppress_state(AppStates::NoteEnableNew as i32); + }, + SaveNoteResult::VersionConflict => { + println!("Failed to save note: version conflict"); + }, + SaveNoteResult::Error(e) => { + println!("Failed to save note: {}", e); + }, + SaveNoteResult::ValidationError(s) => { + println!("Failed to save note: validation error: {}", s); + } + SaveNoteResult::FileAlreadyExists(path) => { + println!("Failed to save note: file already exists: {}", path.display()); + } + SaveNoteResult::FileError(e) => { + println!("Failed to save note: file error: {}", e); + } + } + }); + }); + + self.modified = false; + } +} + fn generate_title(s: &str) -> Option<(&str, usize)> { for line in s.lines() { if !line.trim().is_empty() { diff --git a/application/note/src/notebook.rs b/application/note/src/notebook.rs index c5ee2ec..d07e0f5 100644 --- a/application/note/src/notebook.rs +++ b/application/note/src/notebook.rs @@ -34,7 +34,7 @@ use ui_rs::{action, ui_actions, UiModel}; use ui_rs::ui::*; use entity::note::{Model as Note, NoteType}; -use crate::note::new_note_id; +use crate::note::{new_note_id, NoteViewModel}; use crate::window::NavigationItem; /// Each Notebook view model gets a unique instance id, that is mostly used to identify, @@ -80,7 +80,7 @@ impl Notebook { doc.ctx.on_attach_action("notebook_on_attach") }) } - + #[action] pub fn notebook_on_attach(&mut self, event: &mut ActionEvent) { let Some(obj) = &mut event.obj else { @@ -109,10 +109,13 @@ impl Notebook { // doc_ref.get_doc() should never fail here if let Some(mut doc) = self.doc_ref.get_doc() { // save the note - current.doc.save_note(); + let note_proxy = current.doc.doc_proxy(); + note_proxy.call_mainthread(move |_doc, note|{ + note.save_note(); + }); // detach the note - current.doc.detach_from(&mut doc.ctx); + doc.ctx.detach(¤t.doc); self.selected_note = None; } } @@ -179,19 +182,19 @@ impl Notebook { match note.data.kind { NoteType::File => { let note_data = crate::file::FileNote::new(self.instance_id, note.id.clone(), self.collection_id, self.backend.clone()); - NoteDoc::File(note_data.into_doc(¬e.data)) + note_data.into_doc(¬e.data) }, _ => { // default: text note (plain text or md) let note_data = crate::note::Note::new(self.instance_id, note.id.clone(), self.collection_id, self.backend.clone()); - NoteDoc::Note(note_data.into_doc(¬e.data)) + note_data.into_doc(¬e.data) } } }; note.model = Some(doc.clone()); if let Some(mut nb) = self.doc_ref.get_doc() { - doc.attach_to(&mut nb.ctx); + nb.ctx.attach(&doc); if is_new { nb.ctx.call_action("textarea_focus"); } @@ -301,7 +304,7 @@ impl Notebook { // The note is also currently attached // Send a message to the note, that it needs an update let arg = Box::new(update.from); - selection.doc.ctx().call_action_with_parameter("changed_by", arg); + selection.doc.ctx.call_action_with_parameter("changed_by", arg); } else { // Note view model is not attached, we can just discard it and the next // time the note is opened, it is recreated and the content reloaded @@ -420,7 +423,7 @@ pub struct NoteItem { id: NoteId, orig_id: NoteId, data: entity::note::Model, - model: Option, + model: Option>, } impl NoteItem { @@ -453,57 +456,7 @@ impl NoteItem { pub struct NoteSelection { pub id: NoteId, - pub doc: NoteDoc, + pub doc: UiDoc, pub index: usize } -#[derive(Clone)] -pub enum NoteDoc { - Note(UiDoc), - File(UiDoc) -} - -impl NoteDoc { - pub fn ctx(&self) -> &UiContext { - match self { - NoteDoc::Note(doc) => { &doc.ctx } - NoteDoc::File(doc) => { &doc.ctx } - } - } - - pub fn attach_to(&self, ctx: &mut UiContext) { - match &self { - NoteDoc::Note(doc) => { - ctx.attach(doc); - } - NoteDoc::File(doc) => { - ctx.attach(doc); - } - } - } - - pub fn detach_from(&self, ctx: &mut UiContext) { - match &self { - NoteDoc::Note(doc) => { - ctx.detach(doc); - } - NoteDoc::File(doc) => { - ctx.detach(doc); - } - } - } - - pub fn save_note(&self) { - match self { - NoteDoc::Note(doc) => { - let note_proxy = doc.doc_proxy(); - note_proxy.call_mainthread(move |_doc, note|{ - note.save_note(); - }); - } - NoteDoc::File(_) => { - - } - } - } -} \ No newline at end of file diff --git a/ui-rs/src/ui/event.rs b/ui-rs/src/ui/event.rs index 93bf370..506f496 100644 --- a/ui-rs/src/ui/event.rs +++ b/ui-rs/src/ui/event.rs @@ -278,11 +278,6 @@ pub struct ActionEventWrapper { pub target: *mut T } -pub struct ActionEventWrapperBoxed { - pub callback: Box, - pub target: *mut Box -} - pub extern "C" fn action_event_wrapper(e: *const ffi::UiEvent, data: *mut c_void) { unsafe { let wrapper = &mut *(data as *mut ActionEventWrapper); @@ -294,16 +289,6 @@ pub extern "C" fn action_event_wrapper(e: *const ffi::UiEvent, data: *mut c_v } } -pub extern "C" fn action_event_wrapper_boxed(e: *const ffi::UiEvent, data: *mut c_void) { - unsafe { - let wrapper = &mut *(data as *mut ActionEventWrapperBoxed); - let target = &mut *(wrapper.target); - let target_ref = target.as_mut(); - action_callback(e, |event| { - (wrapper.callback)(target_ref, event); - }); - } -} fn action_callback(e: *const ffi::UiEvent, f: F) where F: FnOnce(&mut ActionEvent) { diff --git a/ui-rs/src/ui/toolkit.rs b/ui-rs/src/ui/toolkit.rs index 63e1023..5012b57 100644 --- a/ui-rs/src/ui/toolkit.rs +++ b/ui-rs/src/ui/toolkit.rs @@ -30,7 +30,7 @@ use std::any::Any; use std::ffi::{c_char, c_int, c_void, CStr, CString}; -use crate::ui::{action_event_wrapper, action_event_wrapper_boxed, event, ffi, simple_event_wrapper, ui_object_get_context, ui_object_get_windowdata, ui_reg_destructor, ui_remove_destructor, untyped_event_wrapper, ActionEventWrapper, ActionEventWrapperBoxed, Event, EventWrapper, NoAppData, SimpleEventWrapper, SubList, UntypedEventWrapper, RUST_TYPED_OBJECT_BOX_ANY}; +use crate::ui::{action_event_wrapper, event, ffi, simple_event_wrapper, ui_object_get_context, ui_object_get_windowdata, ui_reg_destructor, ui_remove_destructor, untyped_event_wrapper, ActionEventWrapper, Event, EventWrapper, NoAppData, SimpleEventWrapper, SubList, UntypedEventWrapper, RUST_TYPED_OBJECT_BOX_ANY}; use std::marker::PhantomData; use std::mem; @@ -162,11 +162,14 @@ impl UiContext { // if this is a document context, use the document pointer as target // otherwise it is a UiObject context, in that case we use the window_data as target unsafe { - let doc_ptr = ui_context_document(self.ptr) as *mut T; - let target_ptr = if !doc_ptr.is_null() { - let doc_storage: *mut *mut Box = doc_ptr.cast(); - let target_ptr = *doc_storage; - self.add_action_for_target_boxed(target_ptr, name, f); + let doc_ptr = ui_context_document(self.ptr); + if !doc_ptr.is_null() { + let doc_storage: *mut *mut Box> = doc_ptr.cast(); + let target_box_ptr = *doc_storage; + let target_opt = &mut **target_box_ptr; + if let Some(target_ptr) = target_opt { + self.add_action_for_target(target_ptr, name, f); + } } else { let obj_ptr = ui_context_obj(self.ptr); assert!(!obj_ptr.is_null()); @@ -188,16 +191,6 @@ impl UiContext { } } - pub unsafe fn add_action_for_target_boxed(&self, target_ptr: *mut Box, name: &str, f: F) - where F: FnMut(&mut T, &mut event::ActionEvent) + 'static { - let wrapper = Box::new(ActionEventWrapperBoxed { callback: Box::new(f), target: target_ptr }); - let ptr = self.reg_box(wrapper); - let cstr = CString::new(name).unwrap(); - unsafe { - ui_add_action(self.ptr, cstr.as_ptr(), Some(action_event_wrapper_boxed::), ptr as *mut c_void); - } - } - pub fn add_action_untyped(&self, name: &str, f: F) where F: FnMut(&mut event::ActionEvent) + 'static { unsafe { @@ -260,7 +253,7 @@ pub extern "C" fn destroy_boxed(data: *mut c_void) { pub struct UiDoc { pub ctx: UiContext, - ptr: *mut *mut Box, + ptr: *mut *mut Option>, _marker: PhantomData } @@ -303,14 +296,15 @@ impl UiDoc { pub fn new_from_box2(data: Box, init: F) -> UiDoc where F: FnOnce(&mut T, &UiDoc) { unsafe { - let doc = ui_document_new(mem::size_of::<*mut Box>()); + let doc = ui_document_new(10000 + mem::size_of::<*mut Box>()); let ctx = UiContext::from_ptr(ui_document_context(doc)); ui_app_ref(); ui_reg_destructor(ctx.ptr, std::ptr::null_mut(), doc_app_unref); - let data_ptr = ctx.reg_box(Box::new(data)); // returns *mut Box - let doc_storage: *mut *mut Box = doc.cast(); + let data_ptr = ctx.reg_box(Box::new(Some(data))); // returns *mut Box + let doc_storage: *mut *mut Option> = doc.cast(); *doc_storage = data_ptr; - let doc_ref = &mut *data_ptr; + let doc_ref_option = &mut *data_ptr; + let doc_ref = doc_ref_option.as_mut().unwrap(); doc_ref.init(&ctx); doc_ref.init_actions(&ctx); let doc_handle = UiDoc:: { ctx: ctx, ptr: doc.cast(), _marker: PhantomData }; @@ -319,34 +313,42 @@ impl UiDoc { } } + /// This function should not be used directly, only via the doc_cast macro. + /// + /// It is safe as long as f returns the same Box (with the same heap address). If it returns + /// a completely new Box, things will break (any event wrapper, that have a pointer to the + /// old target pointer). pub fn cast( self, f: impl FnOnce(Box) -> Box, ) -> UiDoc { - unsafe { - // replace the Box container for this document - // the destructor for the previous Box must be removed - ui_remove_destructor(self.ctx.ptr, self.ptr.cast()); - ui_document_ref(self.ptr.cast()); - - let outer: Box> = Box::from_raw(*self.ptr); - - // Prevent dropping the outer box while extracting the inner box - let outer = std::mem::ManuallyDrop::new(outer); + unsafe { - let inner: Box = std::ptr::read(&**outer); - let new_inner: Box = f(inner); + // because the doc is just transformed into a new type, the old doc must + // not be dropped + let doc = std::mem::ManuallyDrop::new(self); - let new_outer: Box> = Box::new(new_inner); + // Get the top level box, that is directly stored in the document and extract the + // inner value, which holds the rust document data object. + let mut outer: Box>> = Box::from_raw(*doc.ptr); + let inner_value = outer.take(); + // Because the old outer box is replaced, it can be dropped in this function, but + // usually there is already a destructor registered for it. Remove the destructor, + // so the outer box can be droped here. + ui_remove_destructor(doc.ctx.ptr, *doc.ptr.cast()); - *self.ptr.cast::<*mut Box>() = Box::into_raw(new_outer); + // Convert Box type and store it in the document + let new_value = inner_value.map(|v|f(v)); + let data_ptr = doc.ctx.reg_box(Box::new(new_value)); + let doc_storage: *mut *mut Option> = doc.ptr.cast(); + *doc_storage = data_ptr; - UiDoc { - ctx: self.ctx.clone(), - ptr: self.ptr.cast(), - _marker: PhantomData, - } + UiDoc { + ctx: doc.ctx.clone(), + ptr: doc_storage, + _marker: PhantomData, + } } } @@ -358,18 +360,23 @@ impl UiDoc { UiDocProxy::from_ptr(self.ptr.cast()) } - pub unsafe fn get_data_ptr(&self) -> *mut Box { + pub unsafe fn get_data_ptr(& self) -> *mut Option> { unsafe { - let box_ptr: *mut *mut Box = self.ptr.cast(); + let box_ptr: *mut *mut Option> = self.ptr.cast(); *box_ptr } } } +/// Converts the document type to a dyn trait +/// +/// Usage: doc_cast!(doc, dyn MyTrait) #[macro_export] macro_rules! doc_cast { ($doc:expr, dyn $trait:path) => { - $doc.cast(|b| Box::new(*b) as Box) + $doc.cast(|b| { + b as Box + }) }; } @@ -382,10 +389,8 @@ impl UiDoc { pub fn new2(data: T, init: F) -> UiDoc where F: FnOnce(&mut T, &UiDoc) { - unsafe { - let inner_box = Box::new(data); - UiDoc::new_from_box2(inner_box, init) - } + let inner_box = Box::new(data); + UiDoc::new_from_box2(inner_box, init) } /* @@ -669,7 +674,10 @@ impl UiDocProxy { let doc: UiDoc = UiDoc::from_ptr(self.ptr); unsafe { let data = doc.get_data_ptr(); - f(&doc, &mut *data); + let data_ref = &mut *data; + if let Some(dat) = data_ref { + f(&doc, &mut *dat); + } } drop(self); }); -- 2.52.0