From e8df83ec21a5e680e69abc3a8a9387eb8e867aa2 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 25 May 2026 07:22:30 +0000 Subject: [PATCH 1/8] Use &Common over &mut Common in some fns --- crates/kas-widgets/src/edit/editor.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index d97115fe7..ea766690a 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -290,7 +290,7 @@ impl Layout for Component { #[inline] fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, _: AlignHints) { - self.0.part.set_rect(&mut self.0.common, cx, rect); + self.0.part.set_rect(&self.0.common, cx, rect); } #[inline] @@ -406,7 +406,7 @@ impl Component { } self.0.part.prepare_runs(&mut self.0.common, &mut self.1); - self.0.part.prepare_wrap(&mut self.0.common); + self.0.part.prepare_wrap(&self.0.common); } /// Fully prepare text for display, ensuring the cursor is within view @@ -586,7 +586,7 @@ impl Part { /// should be very cheap. /// /// Note that editors always use default alignment of content. - pub fn set_rect(&mut self, common: &mut Common, cx: &mut SizeCx, rect: Rect) { + pub fn set_rect(&mut self, common: &Common, cx: &mut SizeCx, rect: Rect) { if rect.size.0 != self.rect.size.0 { self.status = self.status.min(Status::LevelRuns); } @@ -616,7 +616,7 @@ impl Part { /// changes to alignment or the wrap-width. /// /// Returns `true` when the size of the bounding-box changes. - fn prepare_wrap(&mut self, common: &mut Common) -> bool { + fn prepare_wrap(&mut self, common: &Common) -> bool { if self.status < Status::LevelRuns || self.rect.size.0 == 0 { return false; }; From ed39d95b2d71618a880df73bc42075d1b6c91a44 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sun, 31 May 2026 07:16:56 +0000 Subject: [PATCH 2/8] Clippy --- crates/kas-core/src/event/cx/key.rs | 6 +++--- crates/kas-core/src/layout/sizer.rs | 6 +++--- crates/kas-core/src/runner/event_loop.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/kas-core/src/event/cx/key.rs b/crates/kas-core/src/event/cx/key.rs index 9b014a237..b583a9566 100644 --- a/crates/kas-core/src/event/cx/key.rs +++ b/crates/kas-core/src/event/cx/key.rs @@ -411,9 +411,9 @@ impl<'a> EventCx<'a> { let id = widget.id(); log::trace!( "start_key_event: window={id}, physical_key={:?}, logical_key={:?}, without_modifiers={:?}", - &event.physical_key, - &event.logical_key, - &event.key_without_modifiers + event.physical_key, + event.logical_key, + event.key_without_modifiers ); let opt_cmd = self diff --git a/crates/kas-core/src/layout/sizer.rs b/crates/kas-core/src/layout/sizer.rs index 1a586e7ab..1b5853f14 100644 --- a/crates/kas-core/src/layout/sizer.rs +++ b/crates/kas-core/src/layout/sizer.rs @@ -150,9 +150,9 @@ impl SolveCache { ); log::debug!( "find_constraints: min={:?}, ideal={:?}, margins={:?}", - &self.min, - &self.ideal, - &self.margins + self.min, + self.ideal, + self.margins ); self.last_width = self.ideal.0; } diff --git a/crates/kas-core/src/runner/event_loop.rs b/crates/kas-core/src/runner/event_loop.rs index f5fca8ec1..2a9fe0768 100644 --- a/crates/kas-core/src/runner/event_loop.rs +++ b/crates/kas-core/src/runner/event_loop.rs @@ -223,12 +223,12 @@ where while let Some(pending) = self.shared.pending.pop_front() { match pending { Pending::Update => { - for (_, window) in self.windows.iter_mut() { + for window in self.windows.values_mut() { window.update(&self.data); } } Pending::ConfigUpdate(action) => { - for (_, window) in self.windows.iter_mut() { + for window in self.windows.values_mut() { window.config_update(&mut self.shared, &self.data, action); } } From 8cb64381012e50b7d87d66c0e0373ff033118501 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 27 May 2026 08:31:45 +0000 Subject: [PATCH 3/8] Fix fn text_index_nearest's binary search algorithm --- crates/kas-widgets/src/edit/editor.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index ea766690a..dbc60fe56 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -1177,19 +1177,15 @@ impl Common { } if coord.1 < y0 { - if p <= l_bound { - break; - } u_bound = p; - p = l_bound + (p - l_bound) / 2; } else if y1 < coord.1 { - if p >= u_bound { - break; - } l_bound = p; - p = p + (u_bound - p) / 2; - } else { + } + let q = l_bound + (u_bound - l_bound) / 2; + if p == q { break; + } else { + p = q; } } From e9ff6799249392c4466a2121ea0d2795fa39519a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 27 May 2026 14:27:25 +0000 Subject: [PATCH 4/8] Make Common, Part types pub --- crates/kas-widgets/src/edit/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/kas-widgets/src/edit/mod.rs b/crates/kas-widgets/src/edit/mod.rs index 266b30b9b..db3bfcefa 100644 --- a/crates/kas-widgets/src/edit/mod.rs +++ b/crates/kas-widgets/src/edit/mod.rs @@ -13,7 +13,7 @@ pub mod highlight; pub use edit_box::EditBox; pub use edit_field::EditBoxCore; -pub use editor::Editor; +pub use editor::{Common, Editor, Part}; pub use guard::*; use kas::cast::Cast; From a99aaedf0c0d095d87d385a08ffd3a17d6150f2e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 26 May 2026 14:15:21 +0000 Subject: [PATCH 5/8] Add pub fns Common::direction, set_direction, set_colors --- crates/kas-widgets/src/edit/editor.rs | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index dbc60fe56..0ccf92fc6 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -153,12 +153,43 @@ impl Common { } } + /// Get the base text direction + #[inline] + pub fn direction(&self) -> Direction { + self.direction + } + + /// Set the base text direction + #[inline] + pub fn set_direction(&mut self, direction: Direction) -> ActionResetStatus { + self.direction = direction; + ActionResetStatus + } + + /// Update the base text direction if in automatic mode to reflect the + /// current text direction of the `part` + pub fn update_direction(&mut self, part: &Part) { + if self.direction.is_auto() { + self.direction = if part.display.text_is_rtl() { + Direction::AutoRtl + } else { + Direction::Auto + }; + } + } + /// Read highlighter colors #[inline] pub fn colors(&self) -> &SchemeColors { &self.colors } + /// Set highlighter colors + #[inline] + pub fn set_colors(&mut self, colors: SchemeColors) { + self.colors = colors; + } + /// Get the theme-defined background color #[inline] pub fn background_color(&self) -> Background { From 33e7edc9d7d0f47e9d27ca3009a6474342a22bca Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 26 May 2026 13:51:58 +0000 Subject: [PATCH 6/8] Tweak Part fns prepare_runs, prepare_wrap --- crates/kas-widgets/src/edit/editor.rs | 88 ++++++++++++++------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 0ccf92fc6..3126f7c6e 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -420,7 +420,14 @@ impl Component { self.0.part.require_reprepare(); } - self.0.part.prepare_runs(&mut self.0.common, &mut self.1); + self.prepare_runs(); + } + + fn prepare_runs(&mut self) { + if self.0.part.status < Status::LevelRuns { + self.0.part.prepare_runs(&self.0.common, &mut self.1); + self.0.common.update_direction(&self.0.part); + } } /// Fully prepare text for display @@ -436,7 +443,7 @@ impl Component { return; } - self.0.part.prepare_runs(&mut self.0.common, &mut self.1); + self.prepare_runs(); self.0.part.prepare_wrap(&self.0.common); } @@ -460,7 +467,7 @@ impl Component { /// modify `self`. #[inline] pub fn measure_height(&mut self, wrap_width: f32, max_lines: Option) -> f32 { - self.0.part.prepare_runs(&mut self.0.common, &mut self.1); + self.prepare_runs(); self.0.part.display.measure_height(wrap_width, max_lines) } @@ -527,6 +534,12 @@ impl Part { self.status == Status::Ready } + /// Check the [`Status`] directly + #[inline] + pub fn status(&self) -> Status { + self.status + } + /// Force full repreparation of text #[inline] pub fn require_reprepare(&mut self) { @@ -536,39 +549,26 @@ impl Part { /// Perform run-breaking and shaping /// /// This represents a high-level step of preparation required before - /// displaying text. This method should be called before any sizing - /// operations. This will advance the [`Status`] to [`Status::LevelRuns`]. - /// This method must be called again after any edits to the `Part`'s text. + /// displaying text: it advances the [`Self::status`] to + /// [`Status::LevelRuns`]. This method is safe to call from any status, + /// though it is suggested to only call when the status is less than + /// [`Status::LevelRuns`]. #[inline] - pub fn prepare_runs(&mut self, common: &mut Common, highlighter: &mut H) { - fn inner(part: &mut Part, common: &mut Common, highlighter: &mut H) { - part.highlight.highlight(&part.text, highlighter); - - let text = part.text.as_str(); - let font_tokens = part.highlight.font_tokens(common.dpem, common.font); - match part.status { - Status::New => part - .display - .prepare_runs(text, common.direction, font_tokens) - .expect("no suitable font found"), - Status::ResizeLevelRuns => part.display.resize_runs(text, font_tokens), - _ => return, - } - - part.status = Status::LevelRuns; - - if common.direction.is_auto() { - common.direction = if part.display.text_is_rtl() { - Direction::AutoRtl - } else { - Direction::Auto - }; - } + pub fn prepare_runs(&mut self, common: &Common, highlighter: &mut H) { + self.highlight.highlight(&self.text, highlighter); + + let text = self.text.as_str(); + let font_tokens = self.highlight.font_tokens(common.dpem, common.font); + match self.status { + Status::New => self + .display + .prepare_runs(text, common.direction, font_tokens) + .expect("no suitable font found"), + Status::ResizeLevelRuns => self.display.resize_runs(text, font_tokens), + _ => return, } - if self.status < Status::LevelRuns { - inner(self, common, highlighter); - } + self.status = Status::LevelRuns; } /// Get the assigned [`Rect`] @@ -640,14 +640,15 @@ impl Part { /// Perform line wrapping and alignment /// /// This represents a high-level step of preparation required before - /// displaying text. After [run-breaking](Self::prepare_runs), this method - /// should be called before displaying the text. This will advance - /// the [status](ConfiguredDisplay::status) to [`Status::Ready`]. - /// This method must be called again after [`Self::prepare_runs`] and after - /// changes to alignment or the wrap-width. + /// displaying text: it advances [`Self::status`] from [`Status::LevelRuns`] + /// to [`Status::Ready`]. + /// + /// If [`Self::status`] is less than [`Status::LevelRuns`], this method + /// returns early, otherwise it performs line-wrapping (if required) and + /// sets the status to [`Status::Ready`]. /// /// Returns `true` when the size of the bounding-box changes. - fn prepare_wrap(&mut self, common: &Common) -> bool { + pub fn prepare_wrap(&mut self, common: &Common) -> bool { if self.status < Status::LevelRuns || self.rect.size.0 == 0 { return false; }; @@ -1149,9 +1150,14 @@ impl Common { ) { let mut any_resized = false; - for part in parts.iter_mut() { + for (i, part) in parts.iter_mut().enumerate() { if !part.is_prepared() { - part.prepare_runs(self, highlighter); + if part.status < Status::LevelRuns { + part.prepare_runs(self, highlighter); + if i == 0 { + self.update_direction(part); + } + } any_resized |= part.prepare_wrap(self); } } From a156696b5a2fdcc852621e67e7e973b0de12ff04 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 26 May 2026 14:20:15 +0000 Subject: [PATCH 7/8] impl From for Part --- crates/kas-widgets/src/edit/editor.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 3126f7c6e..f95b07dca 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -512,6 +512,15 @@ impl Default for Part { } } +impl From for Part { + fn from(text: S) -> Self { + Part { + text: Rc::new(text.to_string()), + ..Default::default() + } + } +} + impl Part { /// Get text contents #[inline] @@ -1117,10 +1126,7 @@ impl Common { }; part.replace_range(b_start..b_end, line); } else { - parts.insert(p, Part { - text: Rc::new(line.to_string()), - ..Default::default() - }); + parts.insert(p, Part::from(line)); } p += 1; b_start = 0; From 73921270cf6de164c535404f6edacd6fef0f0679 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 27 May 2026 10:33:38 +0000 Subject: [PATCH 8/8] Add fns Editor::clone_text(&self) -> Rc, set_text(&mut self, text: Rc) --- crates/kas-widgets/src/dialog.rs | 4 +- crates/kas-widgets/src/edit/editor.rs | 67 +++++++++++++++++++++++---- examples/data-list-view.rs | 2 +- examples/data-list.rs | 6 +-- examples/gallery.rs | 17 ++++--- examples/text-editor/text-editor.rs | 2 +- 6 files changed, 72 insertions(+), 26 deletions(-) diff --git a/crates/kas-widgets/src/dialog.rs b/crates/kas-widgets/src/dialog.rs index 4d6cd8896..5b9079441 100644 --- a/crates/kas-widgets/src/dialog.rs +++ b/crates/kas-widgets/src/dialog.rs @@ -280,7 +280,7 @@ mod AlertUnsaved { #[derive(Debug)] pub enum TextEditResult { Cancel, - Ok(String), + Ok(std::rc::Rc), } #[derive(Clone, Debug)] @@ -334,7 +334,7 @@ mod TextEdit { fn close(&mut self, cx: &mut EventCx, commit: bool) -> IsUsed { cx.push(if commit { - TextEditResult::Ok(self.edit.clone_string()) + TextEditResult::Ok(self.edit.clone_text()) } else { TextEditResult::Cancel }); diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index f95b07dca..c52da5921 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -528,6 +528,20 @@ impl Part { self.text.as_str() } + /// Get text contents as a reference to the internal [`Rc`] + #[inline] + pub fn text(&self) -> &Rc { + &self.text + } + + /// Clone text contents + /// + /// Text is stored using [`Rc`] internally. + #[inline] + pub fn clone_text(&self) -> Rc { + Rc::clone(&self.text) + } + /// Get the base directionality of the text /// /// [`Self::prepare_runs`] should be called before this method. @@ -1529,12 +1543,12 @@ impl Common { self.last_edit = edit; let (part, texts) = match edit { None | Some(EditOp::Initial) | Some(EditOp::Cursor) => (0, vec![]), - Some(EditOp::Ime(part)) => (part, vec![Rc::clone(&parts.get(part).text)]), + Some(EditOp::Ime(part)) => (part, vec![parts.get(part).clone_text()]), Some(EditOp::KeyInput(start, last)) | Some(EditOp::KeyDelete(start, last)) | Some(EditOp::Replace(start, last)) => { let texts = (start..last + 1) - .map(|part| Rc::clone(&parts.get(part).text)) + .map(|part| parts.get(part).clone_text()) .collect(); (start, texts) } @@ -1989,10 +2003,18 @@ impl Editor { self.part.text.as_str() } - /// Get the text contents as a `String` + /// Get text contents as a reference to the internal [`Rc`] #[inline] - pub fn clone_string(&self) -> String { - self.as_str().to_string() + pub fn text(&self) -> &Rc { + self.part.text() + } + + /// Clone text contents + /// + /// Text is stored using [`Rc`] internally. + #[inline] + pub fn clone_text(&self) -> Rc { + self.part.clone_text() } /// Get the (horizontal) text direction @@ -2021,7 +2043,9 @@ impl Editor { /// Set text contents from a `str` /// - /// Returns `true` if the text may have changed. + /// This method does not call action handlers on the guard. + /// + /// Returns `true` if the text contents changed. #[inline] pub fn set_str(&mut self, cx: &mut EventState, text: &str) -> bool { if self.as_str() != text { @@ -2035,17 +2059,40 @@ impl Editor { /// Set text contents from a `String` /// /// This method does not call action handlers on the guard. - pub fn set_string(&mut self, cx: &mut EventState, text: String) { - if self.as_str() == text { - return; // no change + /// + /// Returns `true` if the text contents changed. + #[inline] + pub fn set_string(&mut self, cx: &mut EventState, text: String) -> bool { + if self.as_str() != text { + self.set_text_unchecked(cx, Rc::new(text)); + true + } else { + false + } + } + + /// Set text contents from an `Rc` + /// + /// This method does not call action handlers on the guard. + /// + /// Returns `true` if the text contents changed. + #[inline] + pub fn set_text(&mut self, cx: &mut EventState, text: Rc) -> bool { + if self.part.text != text { + self.set_text_unchecked(cx, text); + true + } else { + false } + } + fn set_text_unchecked(&mut self, cx: &mut EventState, text: Rc) { self.common .save_undo_state(&mut self.part, Some(EditOp::Replace(0, 0))); self.common.cancel_selection_and_ime(&mut self.part, cx); - self.part.text = Rc::new(text); + self.part.text = text; self.part.require_reprepare(); let len = TextIndex::new(0, self.as_str().len()); diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index 7b0159cd1..54d2daec6 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -109,7 +109,7 @@ impl EditGuard for ListEntryGuard { } fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &MyItem) { - cx.push(Control::Update(self.0, edit.clone_string())); + cx.push(Control::Update(self.0, edit.as_str().to_string())); } } diff --git a/examples/data-list.rs b/examples/data-list.rs index 9cb3ab4b4..a32fee98f 100644 --- a/examples/data-list.rs +++ b/examples/data-list.rs @@ -62,7 +62,7 @@ impl Data { return; } Control::UpdateCurrent(text) => { - self.active_string = text.clone(); + self.active_string = text; return; } }; @@ -88,7 +88,7 @@ impl EditGuard for ListEntryGuard { fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Data) { if data.active == self.0 { - cx.push(Control::UpdateCurrent(edit.clone_string())); + cx.push(Control::UpdateCurrent(edit.as_str().to_string())); } } } @@ -117,7 +117,7 @@ mod ListEntry { fn handle_messages(&mut self, cx: &mut EventCx, data: &Data) { if let Some(SelectEntry(n)) = cx.try_pop() { if data.active != n { - cx.push(Control::Select(n, self.edit.clone_string())); + cx.push(Control::Select(n, self.edit.as_str().to_string())); } } } diff --git a/examples/gallery.rs b/examples/gallery.rs index 7c0f98b7a..f73a6ab81 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -21,6 +21,7 @@ use kas::widgets::edit::{EditGuard, Editor, highlight::SyntectHighlighter}; use kas::widgets::{column, *}; use kas::window::Popup; use std::ops::Range; +use std::rc::Rc; #[derive(Debug, Default)] struct AppData { @@ -63,10 +64,10 @@ fn widgets() -> Page { Check(bool), Combo(Entry), Radio(u32), - Edit(String), + Edit(Rc), Slider(i32), SpinBox(i32), - Text(String), + Text(Rc), } impl_scope! { @@ -78,11 +79,11 @@ fn widgets() -> Page { value: i32 = 5, entry: Entry, ratio: f32 = 0.0, - text: String, + text: Rc, } } let data = Data { - text: "Use button to edit →".to_string(), + text: Rc::new("Use button to edit →".to_string()), ..Default::default() }; @@ -91,7 +92,7 @@ fn widgets() -> Page { type Data = Data; fn activate(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &Data) -> IsUsed { - cx.push(Item::Edit(edit.clone_string())); + cx.push(Item::Edit(edit.clone_text())); Used } @@ -119,11 +120,9 @@ fn widgets() -> Page { fn handle_messages(&mut self, cx: &mut EventCx, data: &Data) { if let Some(MsgEdit) = cx.try_pop() { - // TODO: do not always set text: if this is a true pop-up it - // should not normally lose data. self.popup.inner.edit(cx, |edit, cx| { edit.clear(cx); - edit.set_string(cx, data.text.clone()); + edit.set_text(cx, data.text.clone()); }); // let ed = TextEdit::new(text, true); // cx.add_window::<()>(ed.into_window("Edit text")); @@ -156,7 +155,7 @@ fn widgets() -> Page { ], row![ "Button", - Button::label_msg("&Press me", Item::Button).map_any() + Button::label("&Press me").with(|cx, _| cx.push(Item::Button)).map_any() ], row![ "Button row", diff --git a/examples/text-editor/text-editor.rs b/examples/text-editor/text-editor.rs index 2dbf5c0bf..6e521051b 100644 --- a/examples/text-editor/text-editor.rs +++ b/examples/text-editor/text-editor.rs @@ -192,7 +192,7 @@ mod Editor { if action == EditorAction::Save && let Some(file) = self.file.clone() { - let contents = self.editor.clone_string(); + let contents = self.editor.clone_text(); cx.send_async(self.id(), async move { Saved(file.write(contents.as_bytes()).await) });