diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index 4a26862ef..0518b8c9b 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -127,7 +127,7 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> { /// Text is drawn from `pos` and clipped to `bounding_box`. /// /// The `text` display must be prepared prior to calling this method. - /// Typically this is done using a [`crate::theme::Text`] object. + /// Typically this is done using a [`crate::text::Text`] object. pub fn text_with_color( &mut self, pos: Vec2, @@ -263,7 +263,7 @@ pub trait Draw { /// Text is drawn from `pos` and clipped to `bounding_box`. /// /// The `text` display must be prepared prior to calling this method. - /// Typically this is done using a [`crate::theme::Text`] object. + /// Typically this is done using a [`crate::text::Text`] object. fn text( &mut self, pos: Vec2, @@ -280,7 +280,7 @@ pub trait Draw { /// Draw text decorations (e.g. underlines) /// /// The `text` display must be prepared prior to calling this method. - /// Typically this is done using a [`crate::theme::Text`] object. + /// Typically this is done using a [`crate::text::Text`] object. fn decorate_text( &mut self, pos: Vec2, diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index c0fccdca5..9434b7f63 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -193,7 +193,7 @@ pub trait DrawSharedImpl: Any { /// Text is drawn from `pos` and clipped to `bounding_box`. /// /// The `text` display must be prepared prior to calling this method. - /// Typically this is done using a [`crate::theme::Text`] object. + /// Typically this is done using a [`crate::text::Text`] object. fn draw_text( &mut self, draw: &mut Self::Draw, @@ -208,7 +208,7 @@ pub trait DrawSharedImpl: Any { /// Draw text decorations (e.g. underlines) /// /// The `text` display must be prepared prior to calling this method. - /// Typically this is done using a [`crate::theme::Text`] object. + /// Typically this is done using a [`crate::text::Text`] object. fn decorate_text( &mut self, draw: &mut Self::Draw, diff --git a/crates/kas-core/src/theme/text.rs b/crates/kas-core/src/text/display.rs similarity index 61% rename from crates/kas-core/src/theme/text.rs rename to crates/kas-core/src/text/display.rs index 0ba6de0fc..48940921a 100644 --- a/crates/kas-core/src/theme/text.rs +++ b/crates/kas-core/src/text/display.rs @@ -5,24 +5,19 @@ //! Theme-applied Text element -use cast::CastFloat; - -use super::TextClass; -#[allow(unused)] use super::{DrawCx, SizeCx}; use crate::Layout; -use crate::cast::Cast; -#[allow(unused)] use crate::event::ConfigCx; +use crate::cast::{Cast, CastFloat}; use crate::geom::{Rect, Vec2}; use crate::layout::{AlignHints, AxisInfo, SizeRules, Stretch}; use crate::text::fonts::FontSelector; -use crate::text::format::{Colors, Decoration, EditableText, FormattableText}; +use crate::text::format::FontToken; use crate::text::*; +use crate::theme::{DrawCx, SizeCx, TextClass}; use std::num::NonZeroUsize; -/// Text type-setting object (theme aware) +/// A [`TextDisplay`] plus configuration and state tracking /// /// This struct contains: -/// - A [`FormattableText`] /// - A [`TextDisplay`] /// - A [`FontSelector`] /// - Type-setting configuration. Values have reasonable defaults: @@ -32,15 +27,11 @@ use std::num::NonZeroUsize; /// [`Self::configure`], otherwise using a default size of 16px. /// - Default text direction and alignment is inferred from the text. /// -/// This struct tracks the [`TextDisplay`]'s -/// [state of preparation][TextDisplay#status-of-preparation] and will perform -/// steps as required. Typical usage of this struct is as follows: -/// - Construct with some text and [`TextClass`] -/// - Configure by calling [`Self::configure`] -/// - Size and draw using [`Layout`] methods +/// This struct tracks the +/// [state of preparation][TextDisplay#status-of-preparation], but is unable to +/// perform the first step of preparation (run-breaking) by itself. #[derive(Clone, Debug)] -pub struct Text { - rect: Rect, +pub struct ConfiguredDisplay { font: FontSelector, dpem: f32, class: TextClass, @@ -54,39 +45,50 @@ pub struct Text { direction: Direction, status: Status, + rect: Rect, display: TextDisplay, - text: T, } -/// Implement [`Layout`], using default alignment where alignment is not provided -impl Layout for Text { +impl Layout for ConfiguredDisplay { fn rect(&self) -> Rect { self.rect } + /// [`Self::prepare_runs`] should be called before this method otherwise the + /// result will have zero size. fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules { let rules = if axis.is_horizontal() { - if self.wrap { - let (min, ideal) = cx.wrapped_line_len(self.class, self.dpem); - let bound: i32 = self.measure_width(ideal.cast()).cast_ceil(); + if self.wrap() { + let (min, ideal) = cx.wrapped_line_len(self.class(), self.font_size()); + let bound: i32 = self + .measure_width(ideal.cast()) + .map(|b| b.cast_ceil()) + .unwrap_or_default(); SizeRules::new(bound.min(min), bound.min(ideal), Stretch::Filler) } else { - let bound: i32 = self.measure_width(f32::INFINITY).cast_ceil(); + let bound: i32 = self + .measure_width(f32::INFINITY) + .map(|b| b.cast_ceil()) + .unwrap_or_default(); SizeRules::new(bound, bound, Stretch::Filler) } } else { let wrap_width = self - .wrap + .wrap() .then(|| axis.other().map(|w| w.cast())) .flatten() .unwrap_or(f32::INFINITY); - let bound: i32 = self.measure_height(wrap_width, None).cast_ceil(); + let bound: i32 = self + .measure_height(wrap_width, None) + .map(|b| b.cast_ceil()) + .unwrap_or_default(); SizeRules::new(bound, bound, Stretch::Filler) }; rules.with_margins(cx.text_margins().extract(axis)) } + /// Uses default alignment where alignment is not provided fn set_rect(&mut self, _: &mut SizeCx, rect: Rect, hints: AlignHints) { self.set_align(hints.complete_default().into()); if rect.size != self.rect.size { @@ -97,22 +99,24 @@ impl Layout for Text { } } self.rect = rect; - self.rewrap(); + self.prepare_wrap(); } + /// Text color and decorations are not present here; derivative types will + /// likely need their own implementation of this method. fn draw(&self, mut draw: DrawCx) { - draw.text(self.rect, self); + if let Ok(display) = self.display() { + let rect = self.rect(); + draw.text(rect.pos, rect, display, &[]); + } } } -impl Text { - /// Construct from a text model - /// - /// This struct must be made ready for usage by calling [`Text::prepare`]. +impl ConfiguredDisplay { + /// Construct a new instance #[inline] - pub fn new(text: T, class: TextClass, wrap: bool) -> Self { - Text { - rect: Rect::default(), + pub fn new(class: TextClass, wrap: bool) -> Self { + ConfiguredDisplay { font: FontSelector::default(), dpem: 16.0, class, @@ -120,7 +124,7 @@ impl Text { align: Default::default(), direction: Direction::default(), status: Status::New, - text, + rect: Rect::default(), display: Default::default(), } } @@ -135,27 +139,6 @@ impl Text { self } - /// Access the formattable text object - #[inline] - pub fn text(&self) -> &T { - &self.text - } - - /// Access the formattable text object mutably - /// - /// If the text is changed, one **must** call [`Self::require_reprepare`] - /// after this method then [`Text::prepare`]. - #[inline] - pub fn text_mut(&mut self) -> &mut T { - &mut self.text - } - - /// Deconstruct, taking the embedded text - #[inline] - pub fn take_text(self) -> T { - self.text - } - /// Set the font and font size (dpem) according to configuration /// /// Font selection depends on the [`TextClass`], [theme configuration] and @@ -182,47 +165,12 @@ impl Text { /// Force full repreparation of text /// - /// This may be required after calling [`Self::text_mut`]. + /// This may be required after calling [`Text::text_mut`](super::Text::text_mut). #[inline] pub fn require_reprepare(&mut self) { self.set_max_status(Status::New); } - /// Set the text - /// - /// Returns `true` when new `text` contents do not match old contents. In - /// this case the new `text` is assigned, but the caller must also call - /// [`Text::prepare`] afterwards. - pub fn set_text(&mut self, text: T) -> bool { - if self.text == text { - return false; // no change - } - - self.text = text; - self.set_max_status(Status::New); - true - } - - /// Length of text - /// - /// This is a shortcut to `self.as_str().len()`. - /// - /// It is valid to reference text within the range `0..text_len()`, - /// even if not all text within this range will be displayed (due to runs). - #[inline] - pub fn str_len(&self) -> usize { - self.as_str().len() - } - - /// Access whole text as contiguous `str` - /// - /// It is valid to reference text within the range `0..text_len()`, - /// even if not all text within this range will be displayed (due to runs). - #[inline] - pub fn as_str(&self) -> &str { - self.text.as_str() - } - /// Get text class #[inline] pub fn class(&self) -> TextClass { @@ -266,7 +214,9 @@ impl Text { /// /// Note that effect tokens may further affect the font selector. /// - /// It is necessary to [`prepare`][Self::prepare] the text after calling this. + /// It is necessary to [`prepare`] the text after calling this. + /// + /// [`prepare`]: super::Text::prepare #[inline] pub fn set_font(&mut self, font: FontSelector) { if font != self.font { @@ -293,7 +243,9 @@ impl Text { /// where the dots-per-point is usually `dpp = scale_factor * 96.0 / 72.0` /// on PC platforms, or `dpp = 1` on MacOS (or 2 for retina displays). /// - /// It is necessary to [`prepare`][Self::prepare] the text after calling this. + /// It is necessary to [`prepare`] the text after calling this. + /// + /// [`prepare`]: super::Text::prepare #[inline] pub fn set_font_size(&mut self, dpem: f32) { if dpem != self.dpem { @@ -304,7 +256,7 @@ impl Text { /// Set font size /// - /// This is an alternative to [`Text::set_font_size`]. It is assumed + /// This is an alternative to [`Self::set_font_size`]. It is assumed /// that 72 Points = 1 Inch and the base screen resolution is 96 DPI. /// (Note: MacOS uses a different definition where 1 Point = 1 Pixel.) #[inline] @@ -320,7 +272,9 @@ impl Text { /// Set the base text direction /// - /// It is necessary to [`prepare`][Self::prepare] the text after calling this. + /// It is necessary to [`prepare`] the text after calling this. + /// + /// [`prepare`]: super::Text::prepare #[inline] pub fn set_direction(&mut self, direction: Direction) { if direction != self.direction { @@ -337,10 +291,12 @@ impl Text { /// Set text alignment /// - /// When vertical alignment is [`Align::Default`], [`Self::prepare`] will + /// When vertical alignment is [`Align::Default`], [`prepare`] will /// set the vertical size of this [`Layout`] to that of the text. /// - /// It is necessary to [`prepare`][Self::prepare] the text after calling this. + /// It is necessary to [`prepare`] the text after calling this. + /// + /// [`prepare`]: super::Text::prepare #[inline] pub fn set_align(&mut self, align: (Align, Align)) { if align != self.align { @@ -356,43 +312,31 @@ impl Text { /// Get the base directionality of the text /// /// This does not require that the text is prepared. - pub fn text_is_rtl(&self) -> bool { + pub fn text_is_rtl(&self, text: &str) -> bool { let cached_is_rtl = match self.line_is_rtl(0) { Ok(None) => Some(self.direction == Direction::Rtl), Ok(Some(is_rtl)) => Some(is_rtl), Err(NotReady) => None, }; + #[cfg(not(debug_assertions))] if let Some(cached) = cached_is_rtl { return cached; } - let is_rtl = self.display.text_is_rtl(self.as_str(), self.direction); + let is_rtl = self.unchecked_display().text_is_rtl(text, self.direction()); if let Some(cached) = cached_is_rtl { debug_assert_eq!(cached, is_rtl); } is_rtl } - /// Return the sequence of color effect tokens - /// - /// This forwards to [`FormattableText::color_tokens`]. + /// Get the status of preparation #[inline] - pub fn color_tokens(&self) -> &[(u32, Colors)] { - self.text.color_tokens() + pub fn status(&self) -> Status { + self.status } - /// Return optional sequences of decoration tokens - /// - /// This forwards to [`FormattableText::decorations`]. - #[inline] - pub fn decorations(&self) -> &[(u32, Decoration)] { - self.text.decorations() - } -} - -/// Type-setting operations and status -impl Text { /// Check whether the status is at least `status` #[inline] pub fn check_status(&self, status: Status) -> Result<(), NotReady> { @@ -411,140 +355,117 @@ impl Text { /// repeated. The internally-tracked status is set to the minimum of /// `status` and its previous value. #[inline] - fn set_max_status(&mut self, status: Status) { + pub fn set_max_status(&mut self, status: Status) { self.status = self.status.min(status); } - /// Read the [`TextDisplay`], without checking status - #[inline] - pub fn unchecked_display(&self) -> &TextDisplay { - &self.display - } - - /// Read the [`TextDisplay`], if fully prepared - #[inline] - pub fn display(&self) -> Result<&TextDisplay, NotReady> { - self.check_status(Status::Ready)?; - Ok(self.unchecked_display()) - } - - /// Read the [`TextDisplay`], if at least wrapped + /// Set status #[inline] - pub fn wrapped_display(&self) -> Result<&TextDisplay, NotReady> { - self.check_status(Status::Wrapped)?; - Ok(self.unchecked_display()) - } - - fn prepare_runs(&mut self) { - match self.status { - Status::New => self - .display - .prepare_runs( - self.text.as_str(), - self.direction, - self.text.font_tokens(self.dpem, self.font), - ) - .expect("no suitable font found"), - Status::ResizeLevelRuns => self.display.resize_runs( - self.text.as_str(), - self.text.font_tokens(self.dpem, self.font), - ), - _ => return, - } - - self.status = Status::LevelRuns; + pub(crate) fn set_status(&mut self, status: Status) { + self.status = status; } - /// Measure required width, up to some `max_width` + /// Prepare text: perform run-breaking and shaping /// - /// This method partially prepares the [`TextDisplay`] as required. + /// This method advances the [status of preparation](Self::status) from + /// [`Status::New`] or [`Status::ResizeLevelRuns`] to [`Status::LevelRuns`]. + /// This is the slowest step of text preparation and requires access to the + /// `text` and `font_tokens`. /// - /// This method allows calculation of the width requirement of a text object - /// without full wrapping and glyph placement. Whenever the requirement - /// exceeds `max_width`, the algorithm stops early, returning `max_width`. + /// The `font_tokens` iterator must not be empty and the first token yielded + /// must have [`FontToken::start`] == 0. (Failure to do so will result in an + /// error on debug builds and usage of default values on release builds.) /// - /// The return value is unaffected by alignment and wrap configuration. - pub fn measure_width(&mut self, max_width: f32) -> f32 { - self.prepare_runs(); - self.display.measure_width(max_width) - } - - /// Measure required vertical height, wrapping as configured - /// - /// Stops after `max_lines`, if provided. - /// - /// May partially prepare the text for display, but does not otherwise - /// modify `self`. - pub fn measure_height(&mut self, wrap_width: f32, max_lines: Option) -> f32 { - self.prepare_runs(); - self.display.measure_height(wrap_width, max_lines) - } - - /// Prepare text for display, as necessary - /// - /// [`Self::set_rect`] must be called before this method. - /// - /// Does all preparation steps necessary in order to display or query the - /// layout of this text. Text is aligned within the set [`Rect`]. + /// # Example /// - /// Returns `true` on success when some action is performed, `false` - /// when the text is already prepared. - pub fn prepare(&mut self) -> bool { - if self.is_prepared() { - return false; + /// ```rust + /// # use kas_core::text::{ConfiguredDisplay, format::FontToken}; + /// # use kas_core::theme::TextClass; + /// let mut display = ConfiguredDisplay::new(TextClass::Standard, true); + /// display.prepare_runs("Hello world", std::iter::once(FontToken { + /// start: 0, + /// dpem: 16.0, + /// font: Default::default(), + /// })); + /// display.prepare_wrap(); + /// // display is now ready + /// ``` + pub fn prepare_runs(&mut self, text: &str, font_tokens: impl Iterator) { + let direction = self.direction(); + match self.status() { + Status::New => self + .unchecked_display_mut() + .prepare_runs(text, direction, font_tokens) + .expect("no suitable font found"), + Status::ResizeLevelRuns => self.unchecked_display_mut().resize_runs(text, font_tokens), + _ => return, } - self.prepare_runs(); - debug_assert!(self.status >= Status::LevelRuns); - self.rewrap(); - true + self.set_status(Status::LevelRuns); } - /// Re-wrap + /// Prepare text: perform line wrapping and alignment /// - /// This is a partial form of re-preparation - fn rewrap(&mut self) { - if self.status < Status::LevelRuns { + /// Actions depend on the [status of preparation](Self::status), + /// + /// - If less than [`Status::LevelRuns`], this method will do nothing. + /// [`Self::prepare_runs`] should be called first. + /// - If at exactly [`Status::LevelRuns`], this method will perform line + /// wrapping (see also next item). + /// - If at [`Status::LevelRuns`] or [`Status::Wrapped`], this method will + /// align the text vertically and advance to [`Status::Ready`]. + pub fn prepare_wrap(&mut self) { + if self.status() < Status::LevelRuns { return; } + let align = self.align(); - if self.status == Status::LevelRuns { + if self.status() == Status::LevelRuns { let align_width = self.rect.size.0.cast(); - let wrap_width = if !self.wrap { f32::INFINITY } else { align_width }; - self.display - .prepare_lines(wrap_width, align_width, self.align.0); + let wrap_width = if !self.wrap() { f32::INFINITY } else { align_width }; + self.unchecked_display_mut() + .prepare_lines(wrap_width, align_width, align.0); } - if self.status <= Status::Wrapped { - self.display - .vertically_align(self.rect.size.1.cast(), self.align.1); + if self.status() <= Status::Wrapped { + let h = self.rect.size.1.cast(); + self.unchecked_display_mut().vertically_align(h, align.1); } - self.status = Status::Ready; + self.set_status(Status::Ready); } - /// Re-prepare, requesting a redraw or resize as required - /// - /// The text is prepared and a redraw is requested. If the allocated size is - /// too small, a resize is requested. - /// - /// This is typically called after updating a `Text` object in a widget. - pub fn reprepare_action(&mut self, cx: &mut ConfigCx) { - if self.prepare() { - let (tl, br) = self.display.bounding_box(); - let bounds: Vec2 = self.rect.size.cast(); - if tl.0 < 0.0 || tl.1 < 0.0 || br.0 > bounds.0 || br.1 > bounds.1 { - cx.resize(); - } - } - cx.redraw(); + /// Read the [`TextDisplay`], without checking status + #[inline] + pub fn unchecked_display(&self) -> &TextDisplay { + &self.display + } + + /// Write to the [`TextDisplay`], without checking status + #[inline] + pub fn unchecked_display_mut(&mut self) -> &mut TextDisplay { + &mut self.display + } + + /// Read the [`TextDisplay`], if fully prepared + #[inline] + pub fn display(&self) -> Result<&TextDisplay, NotReady> { + self.check_status(Status::Ready)?; + Ok(self.unchecked_display()) + } + + /// Read the [`TextDisplay`], if at least wrapped + #[inline] + pub fn wrapped_display(&self) -> Result<&TextDisplay, NotReady> { + self.check_status(Status::Wrapped)?; + Ok(self.unchecked_display()) } /// Offset prepared content to avoid left-overhangs /// - /// This might be called after [`Self::prepare`] to ensure content does not - /// overhang to the left (i.e. that the x-component of the first [`Vec2`] - /// returned by [`Self::bounding_box`] is not negative). + /// This might be called after [`prepare`][super::Text::prepare] to ensure + /// content does not overhang to the left (i.e. that the x-component of the + /// first [`Vec2`] returned by [`Self::bounding_box`] is not negative). /// /// This is a special utility intended for content which may be scrolled /// using the size reported by [`Self::bounding_box`]. Note that while @@ -626,57 +547,40 @@ impl Text { Ok(self.wrapped_display()?.line_index_nearest(line, x)) } - /// Find the starting position (top-left) of the glyph at the given index - /// - /// See [`TextDisplay::text_glyph_pos`]. - pub fn text_glyph_pos(&self, index: usize) -> Result { - Ok(self.display()?.text_glyph_pos(index)) - } -} - -/// Text editing operations -impl Text { - /// Insert a `text` at the given position + /// Measure required width, up to some `max_width` /// - /// This may be used to edit the raw text instead of replacing it. - /// One must call [`Text::prepare`] afterwards. + /// This method allows calculation of the width requirement of a text object + /// without full wrapping and glyph placement. Whenever the requirement + /// exceeds `max_width`, the algorithm stops early, returning `max_width`. /// - /// Currently this is not significantly more efficient than - /// [`Text::set_text`]. This may change in the future (TODO). - #[inline] - pub fn insert_str(&mut self, index: usize, text: &str) { - self.text.insert_str(index, text); - self.set_max_status(Status::New); + /// The return value is unaffected by alignment and wrap configuration. + pub fn measure_width(&self, max_width: f32) -> Result { + if self.status >= Status::LevelRuns { + Ok(self.display.measure_width(max_width)) + } else { + Err(NotReady) + } } - /// Replace a section of text - /// - /// This may be used to edit the raw text instead of replacing it. - /// One must call [`Text::prepare`] afterwards. - /// - /// One may simulate an unbounded range by via `start..usize::MAX`. + /// Measure required vertical height, wrapping as configured /// - /// Currently this is not significantly more efficient than - /// [`Text::set_text`]. This may change in the future (TODO). - #[inline] - pub fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { - self.text.replace_range(range, replace_with); - self.set_max_status(Status::New); + /// Stops after `max_lines`, if provided. + pub fn measure_height( + &self, + wrap_width: f32, + max_lines: Option, + ) -> Result { + if self.status >= Status::LevelRuns { + Ok(self.display.measure_height(wrap_width, max_lines)) + } else { + Err(NotReady) + } } - /// Replace the whole text + /// Find the starting position (top-left) of the glyph at the given index /// - /// Returns `true` when new `text` contents do not match old contents. In - /// this case the new `text` is assigned, but the caller must also call - /// [`Text::prepare`] afterwards. - #[inline] - pub fn set_str(&mut self, text: &str) -> bool { - if self.text.as_str() == text { - return false; // no change - } - - self.text.set_str(text); - self.set_max_status(Status::New); - true + /// See [`TextDisplay::text_glyph_pos`]. + pub fn text_glyph_pos(&self, index: usize) -> Result { + Ok(self.display()?.text_glyph_pos(index)) } } diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index a9f5fb5f2..5efa5bb1e 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -160,7 +160,7 @@ pub trait FormattableText: std::cmp::PartialEq { /// /// These tokens are used to select the font and font size. /// Each text object has a configured - /// [font size][crate::theme::Text::set_font_size] and [`FontSelector`]; these + /// [font size][super::ConfiguredDisplay::set_font_size] and [`FontSelector`]; these /// values are passed as a reference (`dpem` and `font`). /// /// The iterator is expected to yield a stream of tokens such that @@ -258,35 +258,6 @@ impl FormattableText for &F { } } -/// Editable text -pub trait EditableText: FormattableText { - /// Insert a `text` at the given position - fn insert_str(&mut self, index: usize, text: &str); - - /// Replace a section of text - fn replace_range(&mut self, range: std::ops::Range, replace_with: &str); - - /// Replace the whole text - fn set_str(&mut self, text: &str); -} - -impl EditableText for String { - #[inline] - fn insert_str(&mut self, index: usize, text: &str) { - self.insert_str(index, text); - } - - #[inline] - fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { - self.replace_range(range, replace_with); - } - - #[inline] - fn set_str(&mut self, text: &str) { - *self = text.to_string(); - } -} - #[cfg(test)] #[test] fn sizes() { diff --git a/crates/kas-core/src/text/mod.rs b/crates/kas-core/src/text/mod.rs index e8600687f..1a49ba180 100644 --- a/crates/kas-core/src/text/mod.rs +++ b/crates/kas-core/src/text/mod.rs @@ -5,11 +5,8 @@ //! Text functionality //! -//! Most of this module is simply a re-export of the [KAS Text] API, hence the -//! lower level of integration than other parts of the library. -//! -//! See also [`crate::theme::Text`] which provides better integration with KAS -//! theming and widget sizing operations. +//! This module is built over the [KAS Text] API; several items here are direct +//! re-exports. //! //! [KAS Text]: https://github.com/kas-gui/kas-text/ @@ -18,15 +15,17 @@ pub use kas_text::{ TextDisplay, Vec2, fonts, }; +mod display; pub mod format; - /// Glyph rastering #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(docsrs, doc(cfg(internal_doc)))] pub mod raster; - mod selection; -pub use selection::{CursorRange, SelectionHelper}; - mod string; +mod text; + +pub use display::ConfiguredDisplay; +pub use selection::{CursorRange, SelectionHelper}; pub use string::AccessString; +pub use text::Text; diff --git a/crates/kas-core/src/text/selection.rs b/crates/kas-core/src/text/selection.rs index 7570e687c..18770f660 100644 --- a/crates/kas-core/src/text/selection.rs +++ b/crates/kas-core/src/text/selection.rs @@ -5,8 +5,7 @@ //! Tools for text selection -use crate::text::format::FormattableText; -use crate::theme::Text; +use super::ConfiguredDisplay; use kas_macros::autoimpl; use std::ops::Range; use unicode_segmentation::UnicodeSegmentation; @@ -182,40 +181,39 @@ impl SelectionHelper { /// the cursor moves. /// /// The selection is expanded by words or lines (if `lines`). Line expansion - /// requires that text has been prepared ([`Text::prepare`]). - pub fn expand(&mut self, text: &Text, lines: bool) { - let string = text.as_str(); + /// requires that text has been prepared (see [`Text::prepare`][super::Text::prepare]). + pub fn expand(&mut self, text: &str, display: &ConfiguredDisplay, lines: bool) { let mut range = self.edit..self.anchor; if range.start > range.end { std::mem::swap(&mut range.start, &mut range.end); } let (mut start, mut end); if !lines { - end = string[range.start..] + end = text[range.start..] .char_indices() .nth(1) .map(|(i, _)| range.start + i) - .unwrap_or(string.len()); - start = string[0..end] + .unwrap_or(text.len()); + start = text[0..end] .split_word_bound_indices() .next_back() .map(|(index, _)| index) .unwrap_or(0); - end = string[start..] + end = text[start..] .split_word_bound_indices() .find_map(|(index, _)| { let pos = start + index; (pos >= range.end).then_some(pos) }) - .unwrap_or(string.len()); + .unwrap_or(text.len()); } else { - start = match text.find_line(range.start) { + start = match display.find_line(range.start) { Ok(Some(r)) => r.1.start, _ => 0, }; - end = match text.find_line(range.end) { + end = match display.find_line(range.end) { Ok(Some(r)) => r.1.end, - _ => string.len(), + _ => text.len(), }; } diff --git a/crates/kas-core/src/text/text.rs b/crates/kas-core/src/text/text.rs new file mode 100644 index 000000000..274a4558d --- /dev/null +++ b/crates/kas-core/src/text/text.rs @@ -0,0 +1,302 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License in the LICENSE-APACHE file or at: +// https://www.apache.org/licenses/LICENSE-2.0 + +//! Theme-applied Text element + +use super::format::{Colors, Decoration, FormattableText}; +use super::*; +use crate::cast::Cast; +use crate::draw::color::Rgba; +use crate::event::ConfigCx; +use crate::geom::{Rect, Vec2}; +use crate::layout::{AlignHints, AxisInfo, SizeRules}; +use crate::theme::{DrawCx, SizeCx, TextClass}; +use crate::{Layout, autoimpl}; +use std::num::NonZeroUsize; + +/// Text type-setting object +/// +/// This struct contains: +/// - A [`FormattableText`] +/// - A [`ConfiguredDisplay`] (accessible using `*self`) +/// +/// This struct tracks the +/// [state of preparation][TextDisplay#status-of-preparation] and will perform +/// steps as required. Typical usage of this struct is as follows: +/// - Construct with some text and [`TextClass`] +/// - Configure by calling [`ConfiguredDisplay::configure`] +/// - Size and draw using [`Layout`] methods +#[derive(Clone, Debug)] +#[autoimpl(Deref, DerefMut using self.inner)] +pub struct Text { + inner: ConfiguredDisplay, + text: T, +} + +/// Implement [`Layout`], using default alignment where alignment is not provided +impl Layout for Text { + #[inline] + fn rect(&self) -> Rect { + self.inner.rect() + } + + #[inline] + fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules { + self.prepare_runs(); + self.inner.size_rules(cx, axis) + } + + #[inline] + fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) { + self.inner.set_rect(cx, rect, hints); + } + + #[inline] + fn draw(&self, mut draw: DrawCx) { + if let Ok(display) = self.display() { + let rect = self.rect(); + let tokens = self.color_tokens(); + draw.text(rect.pos, rect, display, tokens); + draw.decorate_text(rect.pos, rect, display, self.decorations()); + } + } +} + +impl Text { + /// Construct from a text model + /// + /// This struct must be made ready for usage by calling [`Text::prepare`]. + #[inline] + pub fn new(text: T, class: TextClass, wrap: bool) -> Self { + Text { + inner: ConfiguredDisplay::new(class, wrap), + text, + } + } + + /// Set text class (inline) + /// + /// `TextClass::Edit(false)` has special handling: line wrapping is disabled + /// and the width of self is set to that of the text. + #[inline] + pub fn with_class(mut self, class: TextClass) -> Self { + self.set_class(class); + self + } + + /// Access the formattable text object + #[inline] + pub fn text(&self) -> &T { + &self.text + } + + /// Access the formattable text object mutably + /// + /// If the text is changed, one **must** call + /// self.[require_reprepare][ConfiguredDisplay::require_reprepare]() + /// after this method then [`Text::prepare`]. + #[inline] + pub fn text_mut(&mut self) -> &mut T { + &mut self.text + } + + /// Deconstruct, taking the embedded text + #[inline] + pub fn take_text(self) -> T { + self.text + } + + /// Set the text + /// + /// Returns `true` when new `text` contents do not match old contents. In + /// this case the new `text` is assigned, but the caller must also call + /// [`Text::prepare`] afterwards. + pub fn set_text(&mut self, text: T) -> bool { + if self.text == text { + return false; // no change + } + + self.text = text; + self.set_max_status(Status::New); + true + } + + /// Length of text + /// + /// This is a shortcut to `self.as_str().len()`. + /// + /// It is valid to reference text within the range `0..text_len()`, + /// even if not all text within this range will be displayed (due to runs). + #[inline] + pub fn str_len(&self) -> usize { + self.as_str().len() + } + + /// Access whole text as contiguous `str` + /// + /// It is valid to reference text within the range `0..text_len()`, + /// even if not all text within this range will be displayed (due to runs). + #[inline] + pub fn as_str(&self) -> &str { + self.text.as_str() + } + + /// Get the base directionality of the text + /// + /// This does not require that the text is prepared. + #[inline] + pub fn text_is_rtl(&self) -> bool { + self.inner.text_is_rtl(self.text.as_str()) + } + + /// Return the sequence of color effect tokens + /// + /// This forwards to [`FormattableText::color_tokens`]. + #[inline] + pub fn color_tokens(&self) -> &[(u32, Colors)] { + self.text.color_tokens() + } + + /// Return optional sequences of decoration tokens + /// + /// This forwards to [`FormattableText::decorations`]. + #[inline] + pub fn decorations(&self) -> &[(u32, Decoration)] { + self.text.decorations() + } + + #[inline] + fn prepare_runs(&mut self) { + if self.status() < Status::LevelRuns { + let (dpem, font) = (self.font_size(), self.font()); + self.inner + .prepare_runs(self.text.as_str(), self.text.font_tokens(dpem, font)); + } + } + + /// Measure required width, up to some `max_width` + /// + /// This method partially prepares the [`TextDisplay`] as required. + /// + /// This method allows calculation of the width requirement of a text object + /// without full wrapping and glyph placement. Whenever the requirement + /// exceeds `max_width`, the algorithm stops early, returning `max_width`. + /// + /// The return value is unaffected by alignment and wrap configuration. + pub fn measure_width(&mut self, max_width: f32) -> f32 { + self.prepare_runs(); + self.unchecked_display().measure_width(max_width) + } + + /// Measure required vertical height, wrapping as configured + /// + /// Stops after `max_lines`, if provided. + /// + /// May partially prepare the text for display, but does not otherwise + /// modify `self`. + pub fn measure_height(&mut self, wrap_width: f32, max_lines: Option) -> f32 { + self.prepare_runs(); + self.unchecked_display() + .measure_height(wrap_width, max_lines) + } + + /// Prepare text for display, as necessary + /// + /// [`Self::set_rect`] must be called before this method. + /// + /// Does all preparation steps necessary in order to display or query the + /// layout of this text. Text is aligned within the set [`Rect`]. + /// + /// Returns `true` on success when some action is performed, `false` + /// when the text is already prepared. + #[inline] + pub fn prepare(&mut self) -> bool { + if self.is_prepared() { + return false; + } + + fn inner(this: &mut Text) { + this.prepare_runs(); + debug_assert!(this.status() >= Status::LevelRuns); + this.inner.prepare_wrap(); + } + inner(self); + true + } + + /// Re-prepare, requesting a redraw or resize as required + /// + /// The text is prepared and a redraw is requested. If the allocated size is + /// too small, a resize is requested. + /// + /// This is typically called after updating a `Text` object in a widget. + pub fn reprepare_action(&mut self, cx: &mut ConfigCx) { + if self.prepare() { + let (tl, br) = self.unchecked_display().bounding_box(); + let bounds: Vec2 = self.rect().size.cast(); + if tl.0 < 0.0 || tl.1 < 0.0 || br.0 > bounds.0 || br.1 > bounds.1 { + cx.resize(); + } + } + cx.redraw(); + } + + /// Draw text with specified color + /// + /// The given `color` is used, ignoring [`Self::color_tokens`] + /// Decorations are inferred from [`Text::decorations`]. + pub fn draw_with_color(&self, mut draw: DrawCx, color: Rgba) { + if let Ok(display) = self.display() { + let rect = self.rect(); + let tokens = [(0, format::Colors { + foreground: format::Color::from_rgba(color), + ..Default::default() + })]; + draw.text(rect.pos, rect, display, &tokens); + draw.decorate_text(rect.pos, rect, display, self.decorations()); + } + } +} + +/// Text editing operations +impl Text { + /// Insert a `text` at the given position + /// + /// This may be used to edit the raw text instead of replacing it. + /// One must call [`Text::prepare`] afterwards. + /// + /// Currently this is not significantly more efficient than + /// [`Text::set_text`]. This may change in the future (TODO). + #[inline] + pub fn insert_str(&mut self, index: usize, text: &str) { + self.text.insert_str(index, text); + self.set_max_status(Status::New); + } + + /// Replace a section of text + /// + /// This may be used to edit the raw text instead of replacing it. + /// One must call [`Text::prepare`] afterwards. + /// + /// One may simulate an unbounded range by via `start..usize::MAX`. + /// + /// Currently this is not significantly more efficient than + /// [`Text::set_text`]. This may change in the future (TODO). + #[inline] + pub fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { + self.text.replace_range(range, replace_with); + self.set_max_status(Status::New); + } + + /// Replace the whole text + /// + /// It is recommended to only call this when `text != self.as_str()` since + /// text preparation is relatively slow. + #[inline] + pub fn set_string(&mut self, text: String) { + self.text = text; + self.set_max_status(Status::New); + } +} diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index a767eb06a..c3ee7e4ea 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -7,14 +7,15 @@ use winit::keyboard::Key; -use super::{FrameStyle, MarkStyle, SelectionStyle, SizeCx, Text, ThemeSize}; +use super::{FrameStyle, MarkStyle, SelectionStyle, SizeCx, ThemeSize}; use crate::dir::Direction; -use crate::draw::color::{ParseError, Rgb, Rgba}; +use crate::draw::color::{ParseError, Rgb}; use crate::draw::{Draw, DrawIface, DrawRounded, DrawShared, DrawSharedImpl, ImageId, PassType}; use crate::event::EventState; #[allow(unused)] use crate::event::{Command, ConfigCx}; use crate::geom::{Coord, Offset, Rect}; -use crate::text::{TextDisplay, format, format::FormattableText}; +#[allow(unused)] use crate::text::format::FormattableText; +use crate::text::{TextDisplay, format}; use crate::theme::ColorsLinear; use crate::{Id, Tile, autoimpl}; #[allow(unused)] use crate::{Layout, theme::TextClass}; @@ -273,61 +274,6 @@ impl<'a> DrawCx<'a> { self.h.selection(rect, style); } - /// Draw text - /// - /// Text colors are inferred from [`Text::color_tokens`] and decorations - /// from [`Text::decorations`]. - /// - /// Text is clipped to `rect` and drawn without offset (see - /// [`Self::text_with_position`]). - /// - /// The `text` should be prepared before calling this method. - pub fn text(&mut self, rect: Rect, text: &Text) { - self.text_with_position(rect.pos, rect, text); - } - - /// Draw text with effects and an offset - /// - /// Text colors are inferred from [`Text::color_tokens`] and decorations - /// from [`Text::decorations`]. - /// - /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if - /// the text is not scrolled. - /// - /// The `text` should be prepared before calling this method. - pub fn text_with_position( - &mut self, - pos: Coord, - rect: Rect, - text: &Text, - ) { - if let Ok(display) = text.display() { - let tokens = text.color_tokens(); - self.text_with_colors(pos, rect, display, tokens); - self.decorate_text(pos, rect, display, text.decorations()); - } - } - - /// Draw text with specified color - /// - /// The given `color` is used, ignoring [`Text::color_tokens`] - /// Decorations are inferred from [`Text::decorations`]. - /// - /// Text is clipped to `rect` and drawn without offset (see - /// [`Self::text_with_position`]). - /// - /// The `text` should be prepared before calling this method. - pub fn text_with_color(&mut self, rect: Rect, text: &Text, color: Rgba) { - if let Ok(display) = text.display() { - let tokens = [(0, format::Colors { - foreground: format::Color::from_rgba(color), - ..Default::default() - })]; - self.text_with_colors(rect.pos, rect, display, &tokens); - self.decorate_text(rect.pos, rect, display, text.decorations()); - } - } - /// Draw text with a list of color tokens /// /// Color `tokens` specify both foreground (text) and background colors. @@ -335,14 +281,14 @@ impl<'a> DrawCx<'a> { /// sequence such that `effects[i].0` values are strictly increasing. /// A glyph for index `j` in the source text will use effect `tokens[i].1` /// where `i` is the largest value such that `tokens[i].0 <= j`, or the - /// default value of `format::Colors` if no such `i` exists. + /// default value of [`format::Colors`] if no such `i` exists. /// /// This method does not draw decorations; see also [`Self::decorate_text`]. /// /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if /// the text is not scrolled. #[inline] - pub fn text_with_colors( + pub fn text( &mut self, pos: Coord, rect: Rect, @@ -363,10 +309,8 @@ impl<'a> DrawCx<'a> { /// Draw text decorations (e.g. underlines) /// /// This does not draw the text itself, but requires most of the same inputs - /// as [`Self::text_with_colors`]. - /// - /// The list of `decorations` may come from [`Text::decorations`] or be any - /// other compatible sequence. See also [`FormattableText::decorations`]. + /// as [`Self::text`]. This method may be called any number of times for a + /// single `text`. See also [`FormattableText::decorations`]. pub fn decorate_text( &mut self, pos: Coord, @@ -391,19 +335,16 @@ impl<'a> DrawCx<'a> { /// Draw an edit marker at the given `byte` index on this `text` /// /// The text cursor is draw from `rect.pos` and clipped to `rect`. - /// - /// The `text` should be prepared before calling this method. - pub fn text_cursor( + pub fn text_cursor( &mut self, pos: Coord, rect: Rect, - text: &Text, + display: &TextDisplay, byte: usize, color: Option, ) { - if let Ok(text) = text.display() { - self.h.text_cursor(&self.id, pos, rect, text, byte, color); - } + self.h + .text_cursor(&self.id, pos, rect, display, byte, color); } /// Draw UI element: check box (without label) @@ -553,15 +494,15 @@ pub trait ThemeDraw { /// Draw text with a list of color effects /// + /// Color `tokens` specify both foreground (text) and background colors. + /// Use `&[]` for no effects (uses the theme-default colors), or use a + /// sequence such that `effects[i].0` values are strictly increasing. + /// A glyph for index `j` in the source text will use effect `tokens[i].1` + /// where `i` is the largest value such that `tokens[i].0 <= j`, or the + /// default value of [`format::Colors`] if no such `i` exists. + /// /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if /// the text is not scrolled. - /// - /// *Font* effects (e.g. bold, italics, text size) must be baked into the - /// [`TextDisplay`] during preparation. In contrast, display effects - /// (e.g. color, underline) are applied only when drawing from the provided - /// list of `tokens`. This list may be the result of [`Text::color_tokens`] - /// or any compatible sequence (including `&[]`). See also - /// [`FormattableText::color_tokens`]. fn text( &mut self, id: &Id, @@ -574,10 +515,8 @@ pub trait ThemeDraw { /// Draw text decorations (e.g. underlines) /// /// This does not draw the text itself, but requires most of the same inputs - /// as [`Self::text`]. - /// - /// The list of `decorations` may come from [`Text::decorations`] or be any - /// other compatible sequence. See also [`FormattableText::decorations`]. + /// as [`Self::text`]. This method may be called any number of times for a + /// single `text`. See also [`FormattableText::decorations`]. fn decorate_text( &mut self, id: &Id, @@ -652,18 +591,3 @@ pub trait ThemeDraw { /// Draw an image fn image(&mut self, id: ImageId, rect: Rect); } - -#[cfg(test)] -mod test { - use super::*; - - fn _draw_ext(mut draw: DrawCx) { - // We can't call this method without constructing an actual ThemeDraw. - // But we don't need to: we just want to test that methods are callable. - - let _scale = draw.size_cx().scale_factor(); - - let text = crate::theme::Text::new("sample", TextClass::Label, false); - draw.text(Rect::ZERO, &text) - } -} diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs index 5ba53906e..b381e7e0f 100644 --- a/crates/kas-core/src/theme/mod.rs +++ b/crates/kas-core/src/theme/mod.rs @@ -20,7 +20,6 @@ mod multi; mod simple_theme; mod size; mod style; -mod text; mod theme_dst; mod traits; @@ -35,7 +34,6 @@ pub use multi::MultiTheme; pub use simple_theme::SimpleTheme; pub use size::SizeCx; pub use style::*; -pub use text::Text; pub use theme_dst::ThemeDst; pub use traits::{Theme, Window}; diff --git a/crates/kas-core/src/widgets/label.rs b/crates/kas-core/src/widgets/label.rs index 04c53b9a5..be05f6427 100644 --- a/crates/kas-core/src/widgets/label.rs +++ b/crates/kas-core/src/widgets/label.rs @@ -9,8 +9,9 @@ use super::adapt::MapAny; use crate::event::ConfigCx; use crate::geom::Rect; use crate::layout::AlignHints; +use crate::text::Text; use crate::text::format::FormattableText; -use crate::theme::{SizeCx, Text, TextClass}; +use crate::theme::{SizeCx, TextClass}; use crate::{Events, Layout, Role, RoleCx, Tile}; use kas_macros::impl_self; use std::fmt::Debug; diff --git a/crates/kas-view/src/filter.rs b/crates/kas-view/src/filter.rs index b5a443b18..5188339b3 100644 --- a/crates/kas-view/src/filter.rs +++ b/crates/kas-view/src/filter.rs @@ -97,7 +97,7 @@ pub struct KeystrokeGuard; impl EditGuard for KeystrokeGuard { type Data = (); - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &Self::Data) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &Self::Data) { cx.push(SetFilter(edit.as_str().to_string())); } } @@ -110,7 +110,7 @@ impl EditGuard for AflGuard { type Data = (); #[inline] - fn focus_lost(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &Self::Data) { + fn focus_lost(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &Self::Data) { cx.push(SetFilter(edit.as_str().to_string())); } } diff --git a/crates/kas-widgets/src/access_label.rs b/crates/kas-widgets/src/access_label.rs index 377c51010..ffe8a3774 100644 --- a/crates/kas-widgets/src/access_label.rs +++ b/crates/kas-widgets/src/access_label.rs @@ -7,7 +7,8 @@ #[allow(unused)] use super::Label; use kas::prelude::*; -use kas::theme::{Text, TextClass}; +use kas::text::Text; +use kas::theme::TextClass; #[impl_self] mod AccessLabel { @@ -129,13 +130,13 @@ mod AccessLabel { } fn draw(&self, mut draw: DrawCx) { - let rect = self.text.rect(); - draw.text(rect, &self.text); + self.text.draw(draw.re()); if let Some((key, decoration)) = self.text.text().key() && draw.access_key(&self.target, key) { // Stop on first successful binding and draw if let Ok(display) = self.text.display() { + let rect = self.text.rect(); draw.decorate_text(rect.pos, rect, display, decoration); } } diff --git a/crates/kas-widgets/src/dialog.rs b/crates/kas-widgets/src/dialog.rs index 424ffcc90..9ede22615 100644 --- a/crates/kas-widgets/src/dialog.rs +++ b/crates/kas-widgets/src/dialog.rs @@ -300,6 +300,7 @@ mod TextEdit { /// Emits a [`TextEditResult`] message when the "Ok" or "Cancel" button is /// pressed. When used as a pop-up, it is up to the caller to close on this /// message. + #[autoimpl(Deref using self.edit)] pub struct TextEdit { core: widget_core!(), #[widget] @@ -315,10 +316,15 @@ mod TextEdit { } } - /// Set text, clearing undo history - pub fn set_text(&mut self, cx: &mut EventState, text: impl ToString) { - self.edit.clear(cx); - self.edit.set_string(cx, text.to_string()); + /// Edit text contents + /// + /// See [`EditBox::edit`]. + pub fn edit( + &mut self, + cx: &mut EventCx, + edit: impl FnOnce(&mut Editor, &mut EventCx) -> T, + ) -> T { + self.edit.edit(cx, &(), edit) } /// Build a [`Window`] diff --git a/crates/kas-widgets/src/edit/edit_box.rs b/crates/kas-widgets/src/edit/edit_box.rs index 0925e141c..8b0e6d3ed 100644 --- a/crates/kas-widgets/src/edit/edit_box.rs +++ b/crates/kas-widgets/src/edit/edit_box.rs @@ -35,7 +35,7 @@ mod EditBox { /// /// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset. #[autoimpl(Debug where G: trait, H: trait)] - #[autoimpl(Deref>, DerefMut using self.inner)] + #[autoimpl(Deref using self.inner)] #[widget] pub struct EditBox, H: Highlighter = Plain> { core: widget_core!(), @@ -180,9 +180,10 @@ mod EditBox { } else if self.is_editable() && let Some(SetValueText(string)) = cx.try_pop() { - self.pre_commit(); - self.set_string(cx, string); - self.inner.call_guard_edit(cx, data); + self.edit(cx, data, |edit, cx| { + edit.pre_commit(); + edit.set_string(cx, string); + }); return; } else if let Some(&ReplaceSelectedText(_)) = cx.try_peek() { self.inner.handle_messages(cx, data); @@ -362,7 +363,7 @@ impl EditBox> { M: Debug + 'static, { self.inner.guard = self.inner.guard.with_msg(msg_fn); - self.inner.set_editable(true); + self.inner = self.inner.with_editable(true); self } } @@ -432,4 +433,17 @@ impl EditBox { self.set_width_em(min_em, ideal_em); self } + + /// Edit text contents + /// + /// This method calls the `edit` closure, then [`EditGuard::edit`], then + /// returns the result of calling `edit`. + pub fn edit( + &mut self, + cx: &mut EventCx, + data: &G::Data, + edit: impl FnOnce(&mut Editor, &mut EventCx) -> T, + ) -> T { + self.inner.edit(cx, data, edit) + } } diff --git a/crates/kas-widgets/src/edit/edit_field.rs b/crates/kas-widgets/src/edit/edit_field.rs index 057137fd0..1094a5eea 100644 --- a/crates/kas-widgets/src/edit/edit_field.rs +++ b/crates/kas-widgets/src/edit/edit_field.rs @@ -12,7 +12,7 @@ use kas::messages::{ReplaceSelectedText, SetValueText}; use kas::prelude::*; use kas::theme::{Background, TextClass}; use std::fmt::{Debug, Display}; -use std::ops::DerefMut; +use std::ops::Deref; use std::str::FromStr; #[impl_self] @@ -62,7 +62,6 @@ mod EditField { /// /// This is a [`Viewport`] widget. #[autoimpl(Debug where G: trait, H: trait)] - #[autoimpl(Deref>, DerefMut using self.editor)] #[widget] #[layout(self.editor)] pub struct EditField, H: Highlighter = Plain> { @@ -74,6 +73,13 @@ mod EditField { pub guard: G, } + impl Deref for Self { + type Target = Editor; + fn deref(&self) -> &Self::Target { + &self.editor.0 + } + } + impl Layout for Self { fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules { let (min, mut ideal): (i32, i32); @@ -85,7 +91,6 @@ mod EditField { // Use the height of the first line as a reference let height = self .editor - .text_mut() .measure_height(width.cast(), std::num::NonZero::new(1)); min = (self.lines.0 * height).cast_ceil(); ideal = (self.lines.1 * height).cast_ceil(); @@ -134,7 +139,7 @@ mod EditField { #[inline] fn tooltip(&self) -> Option<&str> { - self.editor.error_message() + self.editor.0.error_message() } fn role(&self, _: &mut dyn RoleCx) -> Role<'_> { @@ -162,17 +167,15 @@ mod EditField { fn configure(&mut self, cx: &mut ConfigCx) { self.editor.configure(cx, self.id()); - self.guard.configure(self.editor.deref_mut(), cx); + self.guard.configure(&mut self.editor.0, cx); } fn update(&mut self, cx: &mut ConfigCx, data: &G::Data) { - let size = self.content_size(); if !self.has_input_focus() { - self.guard.update(self.editor.deref_mut(), cx, data); - } - if size != self.content_size() { - cx.resize(); + self.guard.update(&mut self.editor.0, cx, data); } + + self.editor.prepare(cx); } fn handle_event(&mut self, cx: &mut EventCx, data: &G::Data, event: Event) -> IsUsed { @@ -180,16 +183,20 @@ mod EditField { EventAction::Unused => Unused, EventAction::Used | EventAction::Cursor => Used, EventAction::FocusGained => { - self.guard.focus_gained(self.editor.deref_mut(), cx, data); + self.guard.focus_gained(&mut self.editor.0, cx, data); + self.editor.prepare(cx); Used } EventAction::FocusLost => { - self.guard.focus_lost(self.editor.deref_mut(), cx, data); + self.guard.focus_lost(&mut self.editor.0, cx, data); + self.editor.prepare(cx); Used } EventAction::Activate(code) => { cx.depress_with_key(&self, code); - self.guard.activate(self.editor.deref_mut(), cx, data) + let result = self.guard.activate(&mut self.editor.0, cx, data); + self.editor.prepare(cx); + result } EventAction::Edit => { self.call_guard_edit(cx, data); @@ -204,13 +211,15 @@ mod EditField { } if let Some(SetValueText(string)) = cx.try_pop() { - self.pre_commit(); - self.set_string(cx, string); - self.call_guard_edit(cx, data); + self.edit(cx, data, |edit, cx| { + edit.pre_commit(); + edit.set_string(cx, string); + }); } else if let Some(ReplaceSelectedText(text)) = cx.try_pop() { - self.pre_commit(); - self.replace_selected_text(cx, &text); - self.call_guard_edit(cx, data); + self.edit(cx, data, |edit, cx| { + edit.pre_commit(); + edit.replace_selected_text(cx, &text); + }); } } } @@ -269,16 +278,18 @@ mod EditField { /// Call the [`EditGuard`]'s `activate` method #[inline] pub fn call_guard_activate(&mut self, cx: &mut EventCx, data: &G::Data) { - self.guard.activate(self.editor.deref_mut(), cx, data); + self.guard.activate(&mut self.editor.0, cx, data); + self.editor.prepare(cx); } /// Call the [`EditGuard`]'s `edit` method /// /// This call also clears the error state (see [`Editor::set_error`]). #[inline] - pub fn call_guard_edit(&mut self, cx: &mut EventCx, data: &G::Data) { - self.clear_error(); - self.guard.edit(self.editor.deref_mut(), cx, data); + fn call_guard_edit(&mut self, cx: &mut EventCx, data: &G::Data) { + self.editor.clear_error(); + self.guard.edit(&mut self.editor.0, cx, data); + self.editor.prepare(cx); } } } @@ -296,9 +307,7 @@ impl EditField> { /// Construct a read-only `EditField` displaying some `String` value #[inline] pub fn string(value_fn: impl Fn(&A) -> String + Send + 'static) -> EditField> { - let mut field = EditField::new(StringGuard::new(value_fn)); - field.set_editable(false); - field + EditField::new(StringGuard::new(value_fn)).with_editable(false) } /// Construct an `EditField` for a parsable value (e.g. a number) @@ -352,8 +361,7 @@ impl EditField> { M: Debug + 'static, { self.guard = self.guard.with_msg(msg_fn); - self.set_editable(true); - self + self.with_editable(true) } } @@ -372,7 +380,7 @@ impl EditField { #[inline] #[must_use] pub fn with_editable(mut self, editable: bool) -> Self { - self.set_editable(editable); + self.editor.0.set_editable(editable); self } @@ -383,7 +391,7 @@ impl EditField { #[inline] #[must_use] pub fn with_multi_line(mut self, multi_line: bool) -> Self { - self.editor.text_mut().set_wrap(multi_line); + self.editor.set_wrap(multi_line); self.lines = match multi_line { false => (1.0, 1.0), true => (4.0, 7.0), @@ -395,7 +403,7 @@ impl EditField { #[inline] #[must_use] pub fn with_class(mut self, class: TextClass) -> Self { - self.editor.text_mut().set_class(class); + self.editor.set_class(class); self } @@ -426,4 +434,19 @@ impl EditField { self.set_width_em(min_em, ideal_em); self } + + /// Edit text contents + /// + /// This method calls the `edit` closure, then [`EditGuard::edit`], then + /// returns the result of calling `edit`. + pub fn edit( + &mut self, + cx: &mut EventCx, + data: &G::Data, + edit: impl FnOnce(&mut Editor, &mut EventCx) -> T, + ) -> T { + let result = edit(&mut self.editor.0, cx); + self.call_guard_edit(cx, data); + result + } } diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index bc452add9..8550dd142 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -7,15 +7,22 @@ use super::highlight::{self, Highlighter, SchemeColors}; use super::*; +use kas::cast::Cast; use kas::event::components::{TextInput, TextInputAction}; -use kas::event::{ElementState, FocusSource, Ime, ImePurpose, ImeSurroundingText, Scroll}; -use kas::geom::Vec2; +use kas::event::{ + ConfigCx, ElementState, FocusSource, Ime, ImePurpose, ImeSurroundingText, Scroll, +}; +use kas::geom::{Rect, Vec2}; +use kas::layout::{AlignHints, AxisInfo, SizeRules}; use kas::prelude::*; -use kas::text::format::{Color, FormattableText}; -use kas::text::{CursorRange, NotReady, SelectionHelper, format}; -use kas::theme::{Background, Text, TextClass}; +use kas::text::format::Color; +use kas::text::{ConfiguredDisplay, CursorRange, NotReady, SelectionHelper, Status, format}; +use kas::theme::{Background, DrawCx, SizeCx, TextClass}; use kas::util::UndoStack; +use kas::{Layout, autoimpl}; use std::borrow::Cow; +use std::num::NonZeroUsize; +use std::ops::{Deref, DerefMut}; use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; /// Inner editor component @@ -25,12 +32,13 @@ use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; /// longer be needed once `impl trait` is stabilised for associated types. /// (Alternatively, [`Editor`] could be re-implemented on the above widgets; /// this is preferable in theory but requires a lot of tedious code.) -#[autoimpl(Debug where H: trait)] -pub struct EditorComponent { - // TODO(opt): id, pos are duplicated here since macros don't let us put the core here +#[autoimpl(Debug)] +pub struct Editor { + // TODO(opt): id is duplicated here since macros don't let us put the core here id: Id, editable: bool, - text: Text>, + display: ConfiguredDisplay, + text: String, colors: SchemeColors, selection: SelectionHelper, edit_x_coord: Option, @@ -38,8 +46,7 @@ pub struct EditorComponent { undo_stack: UndoStack<(String, CursorRange)>, has_key_focus: bool, current: CurrentAction, - error_state: bool, - error_message: Option>, + error_state: Option>>, input_handler: TextInput, } @@ -60,25 +67,38 @@ pub struct EditorComponent { /// cannot implement [`Viewport`] directly, but it does provide the following /// methods: [`Self::content_size`], [`Self::draw_with_offset`]. #[autoimpl(Debug where H: trait)] -#[autoimpl(Deref, DerefMut using self.0)] -pub struct Component(EditorComponent); +pub struct Component(pub Editor, highlight::Text); + +impl Deref for Component { + type Target = ConfiguredDisplay; + fn deref(&self) -> &Self::Target { + &self.0.display + } +} + +impl DerefMut for Component { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0.display + } +} impl Layout for Component { #[inline] fn rect(&self) -> Rect { - self.text.rect() + self.0.display.rect() } #[inline] fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules { - self.text.size_rules(cx, axis) + self.prepare_runs(); + self.0.display.size_rules(cx, axis) } fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) { - self.text.set_rect(cx, rect, hints); - self.text.ensure_no_left_overhang(); - if self.current.is_ime_enabled() { - self.set_ime_cursor_area(cx); + self.0.display.set_rect(cx, rect, hints); + self.0.display.ensure_no_left_overhang(); + if self.0.current.is_ime_enabled() { + self.0.set_ime_cursor_area(cx); } } @@ -91,10 +111,11 @@ impl Layout for Component { impl Default for Component { #[inline] fn default() -> Self { - Component(EditorComponent { + let editor = Editor { id: Id::default(), editable: true, - text: Text::new(Default::default(), TextClass::Editor, false), + display: ConfiguredDisplay::new(TextClass::Editor, false), + text: Default::default(), colors: SchemeColors::default(), selection: Default::default(), edit_x_coord: None, @@ -102,10 +123,11 @@ impl Default for Component { undo_stack: UndoStack::new(), has_key_focus: false, current: CurrentAction::None, - error_state: false, - error_message: None, + error_state: None, input_handler: Default::default(), - }) + }; + + Component(editor, Default::default()) } } @@ -114,12 +136,13 @@ impl From for Component { fn from(text: S) -> Self { let text = text.to_string(); let len = text.len(); - let text = highlight::Text::new(H::default(), text); - Component(EditorComponent { - text: Text::new(text, TextClass::Editor, false), + let editor = Editor { + text, selection: SelectionHelper::from(len), ..Self::default().0 - }) + }; + + Component(editor, highlight::Text::new(H::default())) } } @@ -127,90 +150,145 @@ impl Component { /// Replace the highlighter #[inline] pub fn with_highlighter(self, highlighter: H2) -> Component

{ - let class = self.class(); - let wrap = self.multi_line(); - let text = self.0.text.take_text().take_text(); - let text = highlight::Text::new(highlighter, text); - - Component(EditorComponent { - id: self.0.id, - editable: self.0.editable, - text: Text::new(text, class, wrap), - colors: self.0.colors, - selection: self.0.selection, - edit_x_coord: self.0.edit_x_coord, - last_edit: self.0.last_edit, - undo_stack: self.0.undo_stack, - has_key_focus: self.0.has_key_focus, - current: self.0.current, - error_state: self.0.error_state, - error_message: self.0.error_message, - input_handler: self.0.input_handler, - }) + Component(self.0, highlight::Text::new(highlighter)) } /// Set a new highlighter of the same type pub fn set_highlighter(&mut self, highlighter: H) { - self.text.text_mut().set_highlighter(highlighter); + self.1 = highlight::Text::new(highlighter); } /// Get the background color pub fn background_color(&self) -> Background { - if self.error_state { + if self.0.error_state.is_some() { Background::Error - } else if let Some(c) = self.colors.background.as_rgba() { + } else if let Some(c) = self.0.colors.background.as_rgba() { Background::Rgb(c.as_rgb()) } else { Background::Default } } - /// Access text - #[inline] - pub fn text(&self) -> &Text { - &self.text - } - - /// Access text (mut) - /// - /// It is left to the wrapping widget to ensure this is not mis-used. - #[inline] - pub fn text_mut(&mut self) -> &mut Text { - &mut self.text - } - /// Set the initial text (inline) /// /// This method should only be used on a new `Editor`. #[inline] #[must_use] pub fn with_text(mut self, text: impl ToString) -> Self { - debug_assert!(self.current == CurrentAction::None && !self.input_handler.is_selecting()); + debug_assert!( + self.0.current == CurrentAction::None && !self.0.input_handler.is_selecting() + ); let text = text.to_string(); let len = text.len(); - self.text.text_mut().set_text(text); - self.selection.set_cursor(len); + self.0.text = text; + self.0.selection.set_cursor(len); self } /// Configure component #[inline] pub fn configure(&mut self, cx: &mut ConfigCx, id: Id) { - self.id = id; - self.text.text_mut().configure(cx); - self.0.colors = self.text.text().scheme_colors(); + self.0.id = id; + if self.1.configure(cx) { + self.0.display.set_max_status(Status::New); + } + self.0.colors = self.1.scheme_colors(); if self.0.colors.selection_foreground == Color::default() { self.0.colors.selection_foreground = Color::SELECTION; } if self.0.colors.selection_background == Color::default() { self.0.colors.selection_background = Color::SELECTION; } - self.text.configure(&mut cx.size_cx()); + self.0.display.configure(&mut cx.size_cx()); + + self.prepare(cx); + } + + #[inline] + fn prepare_runs(&mut self) { + fn inner(this: &mut Component) { + this.1.highlight(&this.0.text); + let (dpem, font) = (this.0.display.font_size(), this.0.display.font()); + this.0 + .display + .prepare_runs(this.0.text.as_str(), this.1.font_tokens(dpem, font)); + } + + if self.0.display.status() < Status::LevelRuns { + inner(self) + } + } + + /// Prepare text for display, as necessary + /// + /// Requests a resize when required. + /// + /// Returns `true` on success when some action is performed, `false` + /// when the text is already prepared. + #[inline] + pub fn prepare(&mut self, cx: &mut ConfigCx) -> bool { + if self.0.display.is_prepared() { + return false; + } + + fn inner(this: &mut Component, cx: &mut ConfigCx) { + this.prepare_runs(); + debug_assert!(this.0.display.status() >= Status::LevelRuns); + + if this.rect().size.0 != 0 { + let bb = this.0.display.bounding_box(); + this.0.display.prepare_wrap(); + if bb != this.0.display.bounding_box() { + cx.resize(); + } + } + } + inner(self, cx); + true + } + + /// Prepare text + /// + /// Updates the view offset (scroll position) if the content size changes or + /// `force_set_offset`. Requests redraw and resize as appropriate. + fn prepare_and_scroll(&mut self, cx: &mut EventCx, force_set_offset: bool) { + let mut set_offset = force_set_offset; + if !self.0.display.is_prepared() { + let bb = self.0.display.bounding_box(); + + self.prepare_runs(); + self.0.display.prepare_wrap(); + self.0.display.ensure_no_left_overhang(); + + cx.redraw(); + if bb != self.0.display.bounding_box() { + cx.resize(); + set_offset = true; + } + } + + if set_offset { + self.0.set_view_offset_from_cursor(cx); + } + } + + /// Measure required vertical height, wrapping as configured + /// + /// Stops after `max_lines`, if provided. + /// + /// May partially prepare the text for display, but does not otherwise + /// modify `self`. + pub fn measure_height(&mut self, wrap_width: f32, max_lines: Option) -> f32 { + self.prepare_runs(); + self.0 + .display + .unchecked_display() + .measure_height(wrap_width, max_lines) } /// Implementation of [`Viewport::content_size`] pub fn content_size(&self) -> Size { - if let Ok((tl, br)) = self.text.bounding_box() { + if let Ok((tl, br)) = self.0.display.bounding_box() { (br - tl).cast_ceil() } else { Size::ZERO @@ -219,16 +297,16 @@ impl Component { /// Implementation of [`Viewport::draw_with_offset`] pub fn draw_with_offset(&self, mut draw: DrawCx, rect: Rect, offset: Offset) { - let Ok(display) = self.text.display() else { + let Ok(display) = self.0.display.display() else { return; }; let pos = self.rect().pos - offset; - let range: Range = self.selection.range().cast(); + let range: Range = self.0.selection.range().cast(); - let color_tokens = self.text.color_tokens(); + let color_tokens = self.1.color_tokens(); let default_colors = format::Colors { - foreground: self.colors.foreground, + foreground: self.0.colors.foreground, background: None, }; let mut buf = [(0, default_colors); 3]; @@ -241,17 +319,17 @@ impl Component { } } else if color_tokens.is_empty() { buf[1].0 = range.start; - buf[1].1.foreground = self.colors.selection_foreground; - buf[1].1.background = Some(self.colors.selection_background); + buf[1].1.foreground = self.0.colors.selection_foreground; + buf[1].1.background = Some(self.0.colors.selection_background); buf[2].0 = range.end; let r0 = if range.start > 0 { 0 } else { 1 }; &buf[r0..] } else { let set_selection_colors = |colors: &mut format::Colors| { - if colors.foreground == self.colors.foreground { - colors.foreground = self.colors.selection_foreground; + if colors.foreground == self.0.colors.foreground { + colors.foreground = self.0.colors.selection_foreground; } - colors.background = Some(self.colors.selection_background); + colors.background = Some(self.0.colors.selection_background); }; vec.reserve(color_tokens.len() + 2); @@ -308,9 +386,14 @@ impl Component { } &vec }; - draw.text_with_colors(pos, rect, display, tokens); + draw.text(pos, rect, display, tokens); - if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { + let decorations = self.1.decorations(); + if !decorations.is_empty() { + draw.decorate_text(pos, rect, display, decorations); + } + + if let CurrentAction::ImePreedit { edit_range } = self.0.current.clone() { let tokens = [ Default::default(), (edit_range.start, format::Decoration { @@ -323,13 +406,13 @@ impl Component { draw.decorate_text(pos, rect, display, &tokens[r0..]); } - if self.editable && draw.ev_state().has_input_focus(self.id_ref()) == Some(true) { + if self.0.editable && draw.ev_state().has_input_focus(self.0.id_ref()) == Some(true) { draw.text_cursor( pos, rect, - &self.text, - self.selection.edit_index(), - Some(self.colors.cursor), + display, + self.0.selection.edit_index(), + Some(self.0.colors.cursor), ); } } @@ -338,8 +421,8 @@ impl Component { pub fn handle_event(&mut self, cx: &mut EventCx, event: Event) -> EventAction { match event { Event::NavFocus(source) if source == FocusSource::Key => { - if !self.input_handler.is_selecting() { - self.request_key_focus(cx, source); + if !self.0.input_handler.is_selecting() { + self.0.request_key_focus(cx, source); } EventAction::Used } @@ -348,31 +431,31 @@ impl Component { Event::SelFocus(source) => { // NOTE: sel focus implies key focus since we only request // the latter. We must set before calling self.set_primary. - self.has_key_focus = true; + self.0.has_key_focus = true; if source == FocusSource::Pointer { - self.set_primary(cx); + self.0.set_primary(cx); } EventAction::Used } Event::KeyFocus => { - self.has_key_focus = true; - self.set_view_offset_from_cursor(cx); + self.0.has_key_focus = true; + self.0.set_view_offset_from_cursor(cx); - if self.current.is_none() { + if self.0.current.is_none() { let hint = Default::default(); let purpose = ImePurpose::Normal; - let surrounding_text = self.ime_surrounding_text(); - cx.replace_ime_focus(self.id.clone(), hint, purpose, surrounding_text); + let surrounding_text = self.0.ime_surrounding_text(); + cx.replace_ime_focus(self.0.id.clone(), hint, purpose, surrounding_text); EventAction::FocusGained } else { EventAction::Used } } Event::LostKeyFocus => { - self.has_key_focus = false; + self.0.has_key_focus = false; cx.redraw(); - if !self.current.is_ime_enabled() { + if !self.0.current.is_ime_enabled() { EventAction::FocusLost } else { EventAction::Used @@ -380,22 +463,26 @@ impl Component { } Event::LostSelFocus => { // NOTE: we can assume that we will receive Ime::Disabled if IME is active - if !self.selection.is_empty() { - self.save_undo_state(None); - self.selection.set_empty(); + if !self.0.selection.is_empty() { + self.0.save_undo_state(None); + self.0.selection.set_empty(); } - self.input_handler.stop_selecting(); + self.0.input_handler.stop_selecting(); cx.redraw(); EventAction::Used } - Event::Command(cmd, code) => match self.cmd_action(cx, cmd, code) { - Ok(action) => action, + Event::Command(cmd, code) => match self.0.cmd_action(cx, cmd, code) { + Ok(action) => { + self.prepare_and_scroll(cx, true); + action + } Err(NotReady) => EventAction::Used, }, Event::Key(event, false) if event.state == ElementState::Pressed => { if let Some(text) = &event.text { - self.save_undo_state(Some(EditOp::KeyInput)); - if self.received_text(cx, text) == Used { + self.0.save_undo_state(Some(EditOp::KeyInput)); + if self.0.received_text(cx, text) == Used { + self.prepare_and_scroll(cx, false); EventAction::Edit } else { EventAction::Unused @@ -406,8 +493,11 @@ impl Component { .shortcuts() .try_match_event(cx.modifiers(), event); if let Some(cmd) = opt_cmd { - match self.cmd_action(cx, cmd, Some(event.physical_key)) { - Ok(action) => action, + match self.0.cmd_action(cx, cmd, Some(event.physical_key)) { + Ok(action) => { + self.prepare_and_scroll(cx, true); + action + } Err(NotReady) => EventAction::Used, } } else { @@ -417,73 +507,75 @@ impl Component { } Event::Ime(ime) => match ime { Ime::Enabled => { - match self.current { + match self.0.current { CurrentAction::None => { - self.current = CurrentAction::ImeStart; - self.set_ime_cursor_area(cx); + self.0.current = CurrentAction::ImeStart; + self.0.set_ime_cursor_area(cx); } CurrentAction::ImeStart | CurrentAction::ImePreedit { .. } => { // already enabled } CurrentAction::Selection => { // Do not interrupt selection - cx.cancel_ime_focus(self.id_ref()); + cx.cancel_ime_focus(self.0.id_ref()); } } - if !self.has_key_focus { + if !self.0.has_key_focus { EventAction::FocusGained } else { EventAction::Used } } Ime::Disabled => { - self.clear_ime(); - if !self.has_key_focus { + self.0.clear_ime(); + if !self.0.has_key_focus { EventAction::FocusLost } else { EventAction::Used } } Ime::Preedit { text, cursor } => { - self.save_undo_state(None); - let mut edit_range = match self.current.clone() { - CurrentAction::ImeStart if cursor.is_some() => self.selection.range(), + self.0.save_undo_state(None); + let mut edit_range = match self.0.current.clone() { + CurrentAction::ImeStart if cursor.is_some() => self.0.selection.range(), CurrentAction::ImeStart => return EventAction::Used, CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, }; - self.text.replace_range(edit_range.clone(), text); + self.0.replace_range(edit_range.clone(), text); edit_range.end = edit_range.start + text.len(); if let Some((start, end)) = cursor { - self.selection.set_sel_index_only(edit_range.start + start); - self.selection.set_edit_index(edit_range.start + end); + self.0 + .selection + .set_sel_index_only(edit_range.start + start); + self.0.selection.set_edit_index(edit_range.start + end); } else { - self.selection.set_cursor(edit_range.start + text.len()); + self.0.selection.set_cursor(edit_range.start + text.len()); } - self.current = CurrentAction::ImePreedit { + self.0.current = CurrentAction::ImePreedit { edit_range: edit_range.cast(), }; - self.edit_x_coord = None; + self.0.edit_x_coord = None; self.prepare_and_scroll(cx, false); EventAction::Used } Ime::Commit { text } => { - self.save_undo_state(Some(EditOp::Ime)); - let edit_range = match self.current.clone() { - CurrentAction::ImeStart => self.selection.range(), + self.0.save_undo_state(Some(EditOp::Ime)); + let edit_range = match self.0.current.clone() { + CurrentAction::ImeStart => self.0.selection.range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, }; - self.text.replace_range(edit_range.clone(), text); - self.selection.set_cursor(edit_range.start + text.len()); + self.0.replace_range(edit_range.clone(), text); + self.0.selection.set_cursor(edit_range.start + text.len()); - self.current = CurrentAction::ImePreedit { - edit_range: self.selection.range().cast(), + self.0.current = CurrentAction::ImePreedit { + edit_range: self.0.selection.range().cast(), }; - self.edit_x_coord = None; + self.0.edit_x_coord = None; self.prepare_and_scroll(cx, false); EventAction::Edit } @@ -491,9 +583,9 @@ impl Component { before_bytes, after_bytes, } => { - self.save_undo_state(None); - let edit_range = match self.current.clone() { - CurrentAction::ImeStart => self.selection.range(), + self.0.save_undo_state(None); + let edit_range = match self.0.current.clone() { + CurrentAction::ImeStart => self.0.selection.range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, }; @@ -501,9 +593,9 @@ impl Component { if before_bytes > 0 { let end = edit_range.start; let start = end - before_bytes; - if self.as_str().is_char_boundary(start) { - self.text.replace_range(start..end, ""); - self.selection.delete_range(start..end); + if self.0.as_str().is_char_boundary(start) { + self.0.replace_range(start..end, ""); + self.0.selection.delete_range(start..end); } else { log::warn!("buggy IME tried to delete range not at char boundary"); } @@ -512,41 +604,40 @@ impl Component { if after_bytes > 0 { let start = edit_range.end; let end = start + after_bytes; - if self.as_str().is_char_boundary(end) { - self.text.replace_range(start..end, ""); + if self.0.as_str().is_char_boundary(end) { + self.0.replace_range(start..end, ""); } else { log::warn!("buggy IME tried to delete range not at char boundary"); } } - if let Some(text) = self.ime_surrounding_text() { - cx.update_ime_surrounding_text(self.id_ref(), text); + if let Some(text) = self.0.ime_surrounding_text() { + cx.update_ime_surrounding_text(self.0.id_ref(), text); } EventAction::Used } }, Event::PressStart(press) if press.is_tertiary() => { - match press.grab_click(self.id()).complete(cx) { + match press.grab_click(self.0.id()).complete(cx) { Unused => EventAction::Unused, Used => EventAction::Used, } } Event::PressEnd { press, .. } if press.is_tertiary() => { - self.set_cursor_from_coord(cx, press.coord); - self.cancel_selection_and_ime(cx); - self.request_key_focus(cx, FocusSource::Pointer); + self.0.set_cursor_from_coord(cx, press.coord); + self.0.cancel_selection_and_ime(cx); + self.0.request_key_focus(cx, FocusSource::Pointer); if let Some(content) = cx.get_primary() { - self.save_undo_state(Some(EditOp::Clipboard)); + self.0.save_undo_state(Some(EditOp::Clipboard)); - let index = self.selection.edit_index(); - let range = self.trim_paste(&content); + let index = self.0.selection.edit_index(); + let range = self.0.trim_paste(&content); - self.text - .replace_range(index..index, &content[range.clone()]); - self.selection.set_cursor(index + range.len()); - self.edit_x_coord = None; + self.0.replace_range(index..index, &content[range.clone()]); + self.0.selection.set_cursor(index + range.len()); + self.0.edit_x_coord = None; self.prepare_and_scroll(cx, false); EventAction::Edit @@ -562,55 +653,97 @@ impl Component { clear, repeats, } => { - if self.current.is_ime_enabled() { - self.clear_ime(); - cx.cancel_ime_focus(self.id_ref()); + if self.0.current.is_ime_enabled() { + self.0.clear_ime(); + cx.cancel_ime_focus(self.0.id_ref()); } - self.save_undo_state(Some(EditOp::Cursor)); - self.current = CurrentAction::Selection; + self.0.save_undo_state(Some(EditOp::Cursor)); + self.0.current = CurrentAction::Selection; - self.set_cursor_from_coord(cx, coord); - self.selection.set_anchor(clear); + self.0.set_cursor_from_coord(cx, coord); + self.0.selection.set_anchor(clear); if repeats > 1 { - self.0.selection.expand(&self.0.text, repeats >= 3); + self.0.selection.expand( + self.0.text.as_str(), + &self.0.display, + repeats >= 3, + ); } - self.request_key_focus(cx, FocusSource::Pointer); + self.0.request_key_focus(cx, FocusSource::Pointer); EventAction::Used } TextInputAction::PressMove { coord, repeats } => { - if self.current == CurrentAction::Selection { - self.set_cursor_from_coord(cx, coord); + if self.0.current == CurrentAction::Selection { + self.0.set_cursor_from_coord(cx, coord); if repeats > 1 { - self.0.selection.expand(&self.0.text, repeats >= 3); + self.0.selection.expand( + self.0.text.as_str(), + &self.0.display, + repeats >= 3, + ); } } EventAction::Used } TextInputAction::PressEnd { coord } => { - if self.current.is_ime_enabled() { - self.clear_ime(); - cx.cancel_ime_focus(self.id_ref()); + if self.0.current.is_ime_enabled() { + self.0.clear_ime(); + cx.cancel_ime_focus(self.0.id_ref()); } - self.save_undo_state(Some(EditOp::Cursor)); - if self.current == CurrentAction::Selection { - self.set_primary(cx); + self.0.save_undo_state(Some(EditOp::Cursor)); + if self.0.current == CurrentAction::Selection { + self.0.set_primary(cx); } else { - self.set_cursor_from_coord(cx, coord); - self.selection.set_empty(); + self.0.set_cursor_from_coord(cx, coord); + self.0.selection.set_empty(); } - self.current = CurrentAction::None; + self.0.current = CurrentAction::None; - self.request_key_focus(cx, FocusSource::Pointer); + self.0.request_key_focus(cx, FocusSource::Pointer); EventAction::Used } }, } } + + /// Clear the error state + #[inline] + pub fn clear_error(&mut self) { + self.0.error_state = None; + } } -impl EditorComponent { +impl Editor { + /// Insert a `text` at the given position + /// + /// This may be used to edit the raw text instead of replacing it. + /// One must call [`Text::prepare`] afterwards. + /// + /// Currently this is not significantly more efficient than + /// [`Text::set_text`]. This may change in the future (TODO). + #[inline] + fn insert_str(&mut self, index: usize, text: &str) { + self.text.insert_str(index, text); + self.display.set_max_status(Status::New); + } + + /// Replace a section of text + /// + /// This may be used to edit the raw text instead of replacing it. + /// One must call [`Text::prepare`] afterwards. + /// + /// One may simulate an unbounded range by via `start..usize::MAX`. + /// + /// Currently this is not significantly more efficient than + /// [`Text::set_text`]. This may change in the future (TODO). + #[inline] + fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { + self.text.replace_range(range, replace_with); + self.display.set_max_status(Status::New); + } + /// Cancel on-going selection and IME actions /// /// This should be called if e.g. key-input interrupts the current @@ -634,7 +767,7 @@ impl EditorComponent { let action = std::mem::replace(&mut self.current, CurrentAction::None); if let CurrentAction::ImePreedit { edit_range } = action { self.selection.set_cursor(edit_range.start.cast()); - self.text.replace_range(edit_range.cast(), ""); + self.replace_range(edit_range.cast(), ""); } } } @@ -651,22 +784,22 @@ impl EditorComponent { let initial_range = range.clone(); let edit_len = edit_range.clone().map(|r| r.len()).unwrap_or(0); - if let Ok(Some((_, line_range))) = self.text.find_line(range.start) { + if let Ok(Some((_, line_range))) = self.display.find_line(range.start) { range.start = line_range.start; } - if let Ok(Some((_, line_range))) = self.text.find_line(range.end) { + if let Ok(Some((_, line_range))) = self.display.find_line(range.end) { range.end = line_range.end; } if range.len() - edit_len > MAX_TEXT_BYTES { range.end = range.end.min(initial_range.end + MAX_TEXT_BYTES / 2); - while !self.text.as_str().is_char_boundary(range.end) { + while !self.as_str().is_char_boundary(range.end) { range.end -= 1; } if range.len() - edit_len > MAX_TEXT_BYTES { range.start = range.start.max(initial_range.start - MAX_TEXT_BYTES / 2); - while !self.text.as_str().is_char_boundary(range.start) { + while !self.as_str().is_char_boundary(range.start) { range.start += 1; } } @@ -675,10 +808,10 @@ impl EditorComponent { let start = range.start; let mut text = String::with_capacity(range.len() - edit_len); if let Some(er) = edit_range { - text.push_str(&self.text.as_str()[range.start..er.start]); - text.push_str(&self.text.as_str()[er.end..range.end]); + text.push_str(&self.as_str()[range.start..er.start]); + text.push_str(&self.as_str()[er.end..range.end]); } else { - text = self.text.as_str()[range].to_string(); + text = self.as_str()[range].to_string(); } let cursor = self.selection.edit_index().saturating_sub(start); @@ -695,7 +828,7 @@ impl EditorComponent { /// Call to set IME position only while IME is active fn set_ime_cursor_area(&self, cx: &mut EventState) { - if let Ok(text) = self.text.display() { + if let Ok(text) = self.display.display() { let range = match self.current.clone() { CurrentAction::ImeStart => self.selection.range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), @@ -728,7 +861,7 @@ impl EditorComponent { return; }; - cx.set_ime_cursor_area(&self.id, rect + Offset::conv(self.text.rect().pos)); + cx.set_ime_cursor_area(&self.id, rect + Offset::conv(self.display.rect().pos)); } } @@ -747,27 +880,6 @@ impl EditorComponent { .try_push((self.clone_string(), self.cursor_range())); } - /// Prepare text - /// - /// Updates the view offset (scroll position) if the content size changes or - /// `force_set_offset`. Requests redraw and resize as appropriate. - fn prepare_and_scroll(&mut self, cx: &mut EventCx, force_set_offset: bool) { - let bb = self.text.bounding_box(); - if self.text.prepare() { - self.text.ensure_no_left_overhang(); - cx.redraw(); - } - - let mut set_offset = force_set_offset; - if bb != self.text.bounding_box() { - cx.resize(); - set_offset = true; - } - if set_offset { - self.set_view_offset_from_cursor(cx); - } - } - /// Insert `text` at the cursor position /// /// Committing undo state is the responsibility of the caller. @@ -781,15 +893,14 @@ impl EditorComponent { let selection = self.selection.range(); let have_sel = selection.start < selection.end; if have_sel { - self.text.replace_range(selection.clone(), text); + self.replace_range(selection.clone(), text); self.selection.set_cursor(selection.start + text.len()); } else { - self.text.insert_str(index, text); + self.insert_str(index, text); self.selection.set_cursor(index + text.len()); } self.edit_x_coord = None; - self.prepare_and_scroll(cx, false); Used } @@ -827,7 +938,7 @@ impl EditorComponent { let mut shift = cx.modifiers().shift_key(); let mut buf = [0u8; 4]; let cursor = self.selection.edit_index(); - let len = self.text.str_len(); + let len = self.as_str().len(); let multi_line = self.multi_line(); let selection = self.selection.range(); let have_sel = selection.end > selection.start; @@ -867,7 +978,7 @@ impl EditorComponent { Action::Move(selection.start, None) } Command::Left if cursor > 0 => GraphemeCursor::new(cursor, len, true) - .prev_boundary(self.text.as_str(), 0) + .prev_boundary(self.as_str(), 0) .unwrap() .map(|index| Action::Move(index, None)) .unwrap_or(Action::None), @@ -875,14 +986,14 @@ impl EditorComponent { Action::Move(selection.end, None) } Command::Right if cursor < len => GraphemeCursor::new(cursor, len, true) - .next_boundary(self.text.as_str(), 0) + .next_boundary(self.as_str(), 0) .unwrap() .map(|index| Action::Move(index, None)) .unwrap_or(Action::None), Command::WordLeft if cursor > 0 => { - let mut iter = self.text.as_str()[0..cursor].split_word_bound_indices(); + let mut iter = self.as_str()[0..cursor].split_word_bound_indices(); let mut p = iter.next_back().map(|(index, _)| index).unwrap_or(0); - while self.text.as_str()[p..] + while self.as_str()[p..] .chars() .next() .map(|c| c.is_whitespace()) @@ -897,11 +1008,9 @@ impl EditorComponent { Action::Move(p, None) } Command::WordRight if cursor < len => { - let mut iter = self.text.as_str()[cursor..] - .split_word_bound_indices() - .skip(1); + let mut iter = self.as_str()[cursor..].split_word_bound_indices().skip(1); let mut p = iter.next().map(|(index, _)| cursor + index).unwrap_or(len); - while self.text.as_str()[p..] + while self.as_str()[p..] .chars() .next() .map(|c| c.is_whitespace()) @@ -921,13 +1030,13 @@ impl EditorComponent { let x = match self.edit_x_coord { Some(x) => x, None => self - .text + .display .text_glyph_pos(cursor)? .next_back() .map(|r| r.pos.0) .unwrap_or(0.0), }; - let mut line = self.text.find_line(cursor)?.map(|r| r.0).unwrap_or(0); + let mut line = self.display.find_line(cursor)?.map(|r| r.0).unwrap_or(0); // We can tolerate invalid line numbers here! line = match cmd { Command::Up => line.wrapping_sub(1), @@ -939,17 +1048,25 @@ impl EditorComponent { 0..=HALF => len, _ => 0, }; - self.text + self.display .line_index_nearest(line, x)? .map(|index| Action::Move(index, Some(x))) .unwrap_or(Action::Move(nearest_end, None)) } Command::Home if cursor > 0 => { - let index = self.text.find_line(cursor)?.map(|r| r.1.start).unwrap_or(0); + let index = self + .display + .find_line(cursor)? + .map(|r| r.1.start) + .unwrap_or(0); Action::Move(index, None) } Command::End if cursor < len => { - let index = self.text.find_line(cursor)?.map(|r| r.1.end).unwrap_or(len); + let index = self + .display + .find_line(cursor)? + .map(|r| r.1.end) + .unwrap_or(len); Action::Move(index, None) } Command::DocHome if cursor > 0 => Action::Move(0, None), @@ -958,7 +1075,7 @@ impl EditorComponent { Command::Home | Command::End | Command::DocHome | Command::DocEnd => Action::None, Command::PageUp | Command::PageDown if multi_line => { let mut v = self - .text + .display .text_glyph_pos(cursor)? .next_back() .map(|r| r.pos.into()) @@ -967,28 +1084,28 @@ impl EditorComponent { v.0 = x; } const FACTOR: f32 = 2.0 / 3.0; - let mut h_dist = f32::conv(self.text.rect().size.1) * FACTOR; + let mut h_dist = f32::conv(self.display.rect().size.1) * FACTOR; if cmd == Command::PageUp { h_dist *= -1.0; } v.1 += h_dist; - Action::Move(self.text.text_index_nearest(v)?, Some(v.0)) + Action::Move(self.display.text_index_nearest(v)?, Some(v.0)) } Command::Delete | Command::DelBack if editable && have_sel => { Action::Delete(selection.clone(), EditOp::Delete) } Command::Delete if editable => GraphemeCursor::new(cursor, len, true) - .next_boundary(self.text.as_str(), 0) + .next_boundary(self.as_str(), 0) .unwrap() .map(|next| Action::Delete(cursor..next, EditOp::Delete)) .unwrap_or(Action::None), Command::DelBack if editable => GraphemeCursor::new(cursor, len, true) - .prev_boundary(self.text.as_str(), 0) + .prev_boundary(self.as_str(), 0) .unwrap() .map(|prev| Action::Delete(prev..cursor, EditOp::Delete)) .unwrap_or(Action::None), Command::DelWord if editable => { - let next = self.text.as_str()[cursor..] + let next = self.as_str()[cursor..] .split_word_bound_indices() .nth(1) .map(|(index, _)| cursor + index) @@ -996,7 +1113,7 @@ impl EditorComponent { Action::Delete(cursor..next, EditOp::Delete) } Command::DelWordBack if editable => { - let prev = self.text.as_str()[0..cursor] + let prev = self.as_str()[0..cursor] .split_word_bound_indices() .next_back() .map(|(index, _)| index) @@ -1009,11 +1126,11 @@ impl EditorComponent { Action::Move(len, None) } Command::Cut if editable && have_sel => { - cx.set_clipboard((self.text.as_str()[selection.clone()]).into()); + cx.set_clipboard((self.as_str()[selection.clone()]).into()); Action::Delete(selection.clone(), EditOp::Clipboard) } Command::Copy if have_sel => { - cx.set_clipboard((self.text.as_str()[selection.clone()]).into()); + cx.set_clipboard((self.as_str()[selection.clone()]).into()); Action::None } Command::Paste if editable => { @@ -1063,13 +1180,13 @@ impl EditorComponent { } else { index..index }; - self.text.replace_range(range, s); + self.replace_range(range, s); self.selection.set_cursor(index + s.len()); self.edit_x_coord = None; EventAction::Edit } Action::Delete(sel, _) => { - self.text.replace_range(sel.clone(), ""); + self.replace_range(sel.clone(), ""); self.selection.set_cursor(sel.start); self.edit_x_coord = None; EventAction::Edit @@ -1087,7 +1204,9 @@ impl EditorComponent { } Action::UndoRedo(redo) => { if let Some((text, cursor)) = self.undo_stack.undo_or_redo(redo) { - if self.text.set_str(text) { + if self.text.as_str() != text { + self.text = text.clone(); + self.display.set_max_status(Status::New); self.edit_x_coord = None; } self.selection = (*cursor).into(); @@ -1098,7 +1217,6 @@ impl EditorComponent { } }; - self.prepare_and_scroll(cx, true); Ok(action) } @@ -1106,8 +1224,8 @@ impl EditorComponent { /// /// Committing undo state is the responsibility of the caller. fn set_cursor_from_coord(&mut self, cx: &mut EventCx, coord: Coord) { - let rel_pos = (coord - self.text.rect().pos).cast(); - if let Ok(index) = self.text.text_index_nearest(rel_pos) { + let rel_pos = (coord - self.display.rect().pos).cast(); + if let Ok(index) = self.display.text_index_nearest(rel_pos) { if index != self.selection.edit_index() { self.selection.set_edit_index(index); self.set_view_offset_from_cursor(cx); @@ -1121,7 +1239,7 @@ impl EditorComponent { fn set_primary(&self, cx: &mut EventCx) { if self.has_key_focus && !self.selection.is_empty() && cx.has_primary() { let range = self.selection.range(); - cx.set_primary(String::from(&self.text.as_str()[range])); + cx.set_primary(String::from(&self.as_str()[range])); } } @@ -1133,13 +1251,13 @@ impl EditorComponent { fn set_view_offset_from_cursor(&mut self, cx: &mut EventCx) { let cursor = self.selection.edit_index(); if let Some(marker) = self - .text + .display .text_glyph_pos(cursor) .ok() .and_then(|mut m| m.next_back()) { let y0 = (marker.pos.1 - marker.ascent).cast_floor(); - let pos = self.text.rect().pos + Offset(marker.pos.0.cast_nearest(), y0); + let pos = self.display.rect().pos + Offset(marker.pos.0.cast_nearest(), y0); let size = Size(0, i32::conv_ceil(marker.pos.1 - marker.descent) - y0); cx.set_scroll(Scroll::Rect(Rect { pos, size })); } @@ -1147,30 +1265,29 @@ impl EditorComponent { } /// Text editor interface -#[kas::split_impl(for EditorComponent)] -pub trait Editor { +impl Editor { /// Get a reference to the widget's identifier #[inline] - fn id_ref(&self) -> &Id { + pub fn id_ref(&self) -> &Id { &self.id } /// Get the widget's identifier #[inline] - fn id(&self) -> Id { + pub fn id(&self) -> Id { self.id.clone() } /// Get text contents #[inline] - fn as_str(&self) -> &str { + pub fn as_str(&self) -> &str { self.text.as_str() } /// Get the text contents as a `String` #[inline] - fn clone_string(&self) -> String { - self.text.as_str().to_string() + pub fn clone_string(&self) -> String { + self.as_str().to_string() } /// Get the (horizontal) text direction @@ -1179,8 +1296,8 @@ pub trait Editor { /// in other cases (including when the text is empty) it returns `false`. /// TODO: support defaulting to RTL. #[inline] - fn text_is_rtl(&self) -> bool { - self.text.text_is_rtl() + pub fn text_is_rtl(&self) -> bool { + self.display.text_is_rtl(self.as_str()) } /// Commit outstanding changes to the undo history @@ -1188,16 +1305,13 @@ pub trait Editor { /// Call this *before* changing the text with [`Self::set_str`] or /// [`Self::set_string`] to commit changes to the undo history. #[inline] - fn pre_commit(&mut self) { + pub fn pre_commit(&mut self) { self.save_undo_state(Some(EditOp::Synthetic)); } /// Clear text contents and undo history - /// - /// This method does not call any [`EditGuard`] actions; consider also - /// calling [`EditField::call_guard_edit`]. #[inline] - fn clear(&mut self, cx: &mut EventState) { + pub fn clear(&mut self, cx: &mut EventState) { self.last_edit = Some(EditOp::Initial); self.undo_stack.clear(); self.set_string(cx, String::new()); @@ -1208,13 +1322,10 @@ pub trait Editor { /// This does not interact with undo history; see also [`Self::clear`], /// [`Self::pre_commit`]. /// - /// This method does not call any [`EditGuard`] actions; consider also - /// calling [`EditField::call_guard_edit`]. - /// /// Returns `true` if the text may have changed. #[inline] - fn set_str(&mut self, cx: &mut EventState, text: &str) -> bool { - if self.text.as_str() != text { + pub fn set_str(&mut self, cx: &mut EventState, text: &str) -> bool { + if self.as_str() != text { self.set_string(cx, text.to_string()); true } else { @@ -1226,57 +1337,47 @@ pub trait Editor { /// /// This does not interact with undo history or call action handlers on the /// guard. - /// - /// This method clears the error state but does not call any [`EditGuard`] - /// actions; consider also calling [`EditField::call_guard_edit`]. - /// - /// Returns `true` if the text is ready and may have changed. - fn set_string(&mut self, cx: &mut EventState, string: String) -> bool { + pub fn set_string(&mut self, cx: &mut EventState, text: String) { + if self.as_str() == text { + return; // no change + } + self.cancel_selection_and_ime(cx); - if !self.text.set_str(&string) { - return false; - } + self.text = text; + self.display.set_max_status(Status::New); - cx.redraw(self.id()); - let len = self.text.str_len(); + let len = self.as_str().len(); self.selection.set_max_len(len); self.edit_x_coord = None; - self.clear_error(); - self.text.prepare() + self.error_state = None; } /// Replace selected text /// /// This does not interact with undo history or call action handlers on the /// guard. - /// - /// This method clears the error state but does not call any [`EditGuard`] - /// actions; consider also calling [`EditField::call_guard_edit`]. - /// - /// Returns `true` if the text is ready and may have changed. #[inline] - fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) -> bool { + pub fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) { self.cancel_selection_and_ime(cx); let index = self.selection.edit_index(); let selection = self.selection.range(); let have_sel = selection.start < selection.end; if have_sel { - self.text.replace_range(selection.clone(), text); + self.replace_range(selection.clone(), text); self.selection.set_cursor(selection.start + text.len()); } else { - self.text.insert_str(index, text); + self.insert_str(index, text); self.selection.set_cursor(index + text.len()); } self.edit_x_coord = None; - self.clear_error(); - self.text.prepare() + self.error_state = None; } /// Access the cursor index / selection range #[inline] - fn cursor_range(&self) -> CursorRange { + pub fn cursor_range(&self) -> CursorRange { *self.selection } @@ -1285,59 +1386,53 @@ pub trait Editor { /// This does not interact with undo history or call action handlers on the /// guard. #[inline] - fn set_cursor_range(&mut self, range: CursorRange) { + pub fn set_cursor_range(&mut self, range: CursorRange) { self.edit_x_coord = None; self.selection = range.into(); } /// Get whether this `EditField` is editable #[inline] - fn is_editable(&self) -> bool { + pub fn is_editable(&self) -> bool { self.editable } /// Set whether this `EditField` is editable #[inline] - fn set_editable(&mut self, editable: bool) { + pub fn set_editable(&mut self, editable: bool) { self.editable = editable; } /// True if the editor uses multi-line mode #[inline] - fn multi_line(&self) -> bool { - self.text.wrap() + pub fn multi_line(&self) -> bool { + self.display.wrap() } /// Get the text class used #[inline] - fn class(&self) -> TextClass { - self.text.class() + pub fn class(&self) -> TextClass { + self.display.class() } /// Get whether the widget has input focus /// /// This is true when the widget is has keyboard or IME focus. #[inline] - fn has_input_focus(&self) -> bool { + pub fn has_input_focus(&self) -> bool { self.has_key_focus || self.current.is_ime_enabled() } /// Get whether the input state is erroneous #[inline] - fn has_error(&self) -> bool { - self.error_state + pub fn has_error(&self) -> bool { + self.error_state.is_some() } /// Get the error message, if any #[inline] - fn error_message(&self) -> Option<&str> { - self.error_message.as_deref() - } - - /// Clear the error state - fn clear_error(&mut self) { - self.error_state = false; - self.error_message = None; + pub fn error_message(&self) -> Option<&str> { + self.error_state.as_ref().and_then(|state| state.as_deref()) } /// Mark the input as erroneous with an optional message @@ -1348,9 +1443,8 @@ pub trait Editor { /// /// When set, the input field's background is drawn red. If a message is /// supplied, then a tooltip will be available on mouse-hover. - fn set_error(&mut self, cx: &mut EventState, message: Option>) { - self.error_state = true; - self.error_message = message; + pub fn set_error(&mut self, cx: &mut EventState, message: Option>) { + self.error_state = Some(message); cx.redraw(&self.id); } } diff --git a/crates/kas-widgets/src/edit/guard.rs b/crates/kas-widgets/src/edit/guard.rs index f8765fe03..700cfbb0f 100644 --- a/crates/kas-widgets/src/edit/guard.rs +++ b/crates/kas-widgets/src/edit/guard.rs @@ -27,7 +27,7 @@ pub trait EditGuard: Sized { /// Configure guard /// /// This function is called when the attached widget is configured. - fn configure(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx) { + fn configure(&mut self, edit: &mut Editor, cx: &mut ConfigCx) { let _ = (edit, cx); } @@ -38,7 +38,7 @@ pub trait EditGuard: Sized { /// /// This method may also be called on loss of input focus (see /// [`Self::focus_lost`]). - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &Self::Data) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &Self::Data) { let _ = (edit, cx, data); } @@ -53,7 +53,7 @@ pub trait EditGuard: Sized { /// - If the field is editable, calls [`Self::focus_lost`] and returns /// returns [`Used`]. /// - If the field is not editable, returns [`Unused`]. - fn activate(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &Self::Data) -> IsUsed { + fn activate(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Self::Data) -> IsUsed { if edit.is_editable() { self.focus_lost(edit, cx, data); Used @@ -65,7 +65,7 @@ pub trait EditGuard: Sized { /// Focus-gained guard /// /// This function is called when the widget gains keyboard or IME focus. - fn focus_gained(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &Self::Data) { + fn focus_gained(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Self::Data) { let _ = (edit, cx, data); } @@ -76,7 +76,7 @@ pub trait EditGuard: Sized { /// /// The default implementation calls [`Self::update`] since updates are /// inhibited while the editor has input focus. - fn focus_lost(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &Self::Data) { + fn focus_lost(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Self::Data) { self.update(edit, cx, data); } @@ -84,12 +84,11 @@ pub trait EditGuard: Sized { /// /// This function is called after the text is updated (including by keyboard /// input, an undo action or by a message like - /// [`kas::messages::SetValueText`]). The exceptions are setter methods like - /// [`clear`](Editor::clear) and [`set_string`](Editor::set_string). + /// [`kas::messages::SetValueText`]). /// /// The guard may call [`Editor::set_error`] here. /// The error state is cleared immediately before calling this method. - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &Self::Data) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Self::Data) { let _ = (edit, cx, data); } } @@ -155,12 +154,12 @@ mod StringGuard { impl EditGuard for Self { type Data = A; - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &A) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &A) { let string = (self.value_fn)(data); edit.set_string(cx, string); } - fn focus_lost(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &A) { + fn focus_lost(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &A) { if self.edited { self.edited = false; if let Some(ref on_afl) = self.on_afl { @@ -172,7 +171,7 @@ mod StringGuard { } } - fn edit(&mut self, _: &mut dyn Editor, _: &mut EventCx, _: &Self::Data) { + fn edit(&mut self, _: &mut Editor, _: &mut EventCx, _: &Self::Data) { self.edited = true; } } @@ -221,13 +220,13 @@ mod ParseGuard { impl EditGuard for Self { type Data = A; - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &A) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &A) { let value = (self.value_fn)(data); edit.set_string(cx, format!("{value}")); self.parsed = None; } - fn focus_lost(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &A) { + fn focus_lost(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &A) { if let Some(value) = self.parsed.take() { (self.on_afl)(cx, value); } else { @@ -236,7 +235,7 @@ mod ParseGuard { } } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &A) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &A) { self.parsed = edit.as_str().parse().ok(); if self.parsed.is_none() { edit.set_error(cx, Some("parse failure".into())); @@ -282,17 +281,17 @@ mod InstantParseGuard { impl EditGuard for Self { type Data = A; - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &A) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &A) { let value = (self.value_fn)(data); edit.set_string(cx, format!("{value}")); } - fn focus_lost(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &A) { + fn focus_lost(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &A) { // Always reset data on focus loss self.update(edit, cx, data); } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &A) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &A) { let result = edit.as_str().parse(); if result.is_err() { edit.set_error(cx, Some("parse failure".into())); diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index a04d5a5e1..b8ab01c45 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -12,7 +12,7 @@ mod text; pub use syntect::{ SyntaxReference as SyntectSyntax, SyntaxSet as SyntectSyntaxSet, SyntectHighlighter, }; -pub use text::Text; +pub(crate) use text::Text; use kas::event::ConfigCx; use kas::text::fonts::{FontStyle, FontWeight}; @@ -68,6 +68,7 @@ pub trait Highlighter { /// theme / color scheme. /// /// The method should return `true` when the highlighter should be re-run. + #[must_use] fn configure(&mut self, cx: &mut ConfigCx) -> bool; /// Get scheme colors diff --git a/crates/kas-widgets/src/edit/highlight/text.rs b/crates/kas-widgets/src/edit/highlight/text.rs index cbf34a9d6..29c2860eb 100644 --- a/crates/kas-widgets/src/edit/highlight/text.rs +++ b/crates/kas-widgets/src/edit/highlight/text.rs @@ -8,7 +8,7 @@ use super::*; use kas::cast::Cast; use kas::text::fonts::{FontSelector, FontStyle, FontWeight}; -use kas::text::format::{Colors, Decoration, EditableText, FontToken, FormattableText}; +use kas::text::format::{Colors, Decoration, FontToken}; #[derive(Clone, Debug, Default, PartialEq)] struct Fmt { @@ -23,9 +23,8 @@ struct Fmt { /// of the embedded highlighter. #[derive(Clone, Debug)] #[kas::autoimpl(PartialEq ignore self.highlighter)] -pub struct Text { +pub(crate) struct Text { highlighter: H, - text: String, fonts: Vec, colors: Vec<(u32, Colors)>, decorations: Vec<(u32, Decoration)>, @@ -33,34 +32,32 @@ pub struct Text { impl Default for Text { fn default() -> Self { - Self::new(H::default(), "") + Self::new(H::default()) } } impl Text { /// Construct a new instance #[inline] - pub fn new(highlighter: H, text: impl ToString) -> Self { - let mut text = Text { + pub fn new(highlighter: H) -> Self { + Text { highlighter, - text: text.to_string(), - fonts: vec![], + fonts: vec![Fmt::default()], colors: vec![], decorations: vec![], - }; - text.highlight(); - text + } } /// Configure the highlighter /// /// This is called when the widget is configured. It may be used to set the /// theme / color scheme. + /// + /// Returns `true` when the highlighter must be re-run. #[inline] - pub fn configure(&mut self, cx: &mut ConfigCx) { - if self.highlighter.configure(cx) { - self.highlight(); - } + #[must_use] + pub fn configure(&mut self, cx: &mut ConfigCx) -> bool { + self.highlighter.configure(cx) } /// Get scheme colors @@ -71,26 +68,8 @@ impl Text { self.highlighter.scheme_colors() } - /// Set a new highlighter - pub fn set_highlighter(&mut self, highlighter: H) { - let text = std::mem::take(&mut self.text); - *self = Self::new(highlighter, text); - } - - /// Assign new contents - #[inline] - pub fn set_text(&mut self, text: String) { - self.text = text; - self.highlight(); - } - - /// Deconstruct, taking the embedded text - #[inline] - pub fn take_text(self) -> String { - self.text - } - - fn highlight(&mut self) { + /// Highlight the text (from scratch) + pub fn highlight(&mut self, text: &str) { self.fonts.clear(); self.fonts.push(Fmt::default()); self.colors.clear(); @@ -131,20 +110,13 @@ impl Text { state = token; }; - if let Err(err) = self.highlighter.highlight_text(&self.text, &mut push_token) { + if let Err(err) = self.highlighter.highlight_text(text, &mut push_token) { log::error!("Highlighting failed: {err}"); debug_assert!(false, "Highlighter: {err}"); } } -} - -impl FormattableText for Text { - #[inline] - fn as_str(&self) -> &str { - &self.text - } - fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + pub fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { self.fonts.iter().cloned().map(move |fmt| FontToken { start: fmt.start, dpem, @@ -159,33 +131,12 @@ impl FormattableText for Text { /// The default implementation returns `&[]`. #[inline] - fn color_tokens(&self) -> &[(u32, Colors)] { + pub fn color_tokens(&self) -> &[(u32, Colors)] { &self.colors } #[inline] - fn decorations(&self) -> &[(u32, Decoration)] { + pub fn decorations(&self) -> &[(u32, Decoration)] { &self.decorations } } - -impl EditableText for Text { - #[inline] - fn insert_str(&mut self, index: usize, text: &str) { - self.text.insert_str(index, text); - self.highlight(); - } - - #[inline] - fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { - self.text.replace_range(range, replace_with); - self.highlight(); - } - - #[inline] - fn set_str(&mut self, text: &str) { - self.text.clear(); - self.text.push_str(text); - self.highlight(); - } -} diff --git a/crates/kas-widgets/src/edit/mod.rs b/crates/kas-widgets/src/edit/mod.rs index e24dc7f54..1273b69d1 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::EditField; -pub use editor::{Component, Editor, EditorComponent}; +pub use editor::{Component, Editor}; pub use guard::*; use kas::event::PhysicalKey; diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index d170a9b6e..e88779a5b 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -9,9 +9,9 @@ use super::{ScrollBar, ScrollBarMsg}; use kas::event::components::{ScrollComponent, TextInput, TextInputAction}; use kas::event::{CursorIcon, FocusSource, Scroll}; use kas::prelude::*; -use kas::text::SelectionHelper; use kas::text::format::{self, FormattableText}; -use kas::theme::{Text, TextClass}; +use kas::text::{SelectionHelper, Text}; +use kas::theme::TextClass; #[impl_self] mod SelectableText { @@ -84,7 +84,7 @@ mod SelectableText { let r0 = if range.start > 0 { 0 } else { 1 }; &tokens[r0..] }; - draw.text_with_colors(pos, rect, display, tokens); + draw.text(pos, rect, display, tokens); draw.decorate_text(pos, rect, display, self.text.decorations()); } @@ -300,7 +300,8 @@ mod SelectableText { self.set_cursor_from_coord(cx, coord); self.selection.set_anchor(clear); if repeats > 1 { - self.selection.expand(&self.text, repeats >= 3); + self.selection + .expand(self.text.as_str(), &self.text, repeats >= 3); } if !self.has_sel_focus { @@ -311,7 +312,8 @@ mod SelectableText { TextInputAction::PressMove { coord, repeats } => { self.set_cursor_from_coord(cx, coord); if repeats > 1 { - self.selection.expand(&self.text, repeats >= 3); + self.selection + .expand(self.text.as_str(), &self.text, repeats >= 3); } Used } diff --git a/crates/kas-widgets/src/spin_box.rs b/crates/kas-widgets/src/spin_box.rs index 342ff77a2..219892289 100644 --- a/crates/kas-widgets/src/spin_box.rs +++ b/crates/kas-widgets/src/spin_box.rs @@ -11,7 +11,8 @@ use crate::{ }; use kas::messages::{DecrementStep, IncrementStep, ReplaceSelectedText, SetValueF64, SetValueText}; use kas::prelude::*; -use kas::theme::{Background, FrameStyle, MarkStyle, Text, TextClass}; +use kas::text::Text; +use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; use std::ops::RangeInclusive; /// Requirements on type used by [`SpinBox`] @@ -157,13 +158,13 @@ impl SpinGuard { impl EditGuard for SpinGuard { type Data = A; - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &A) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &A) { self.value = (self.state_fn)(cx, data); let text = self.value.to_string(); edit.set_string(cx, text); } - fn focus_lost(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &A) { + fn focus_lost(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &A) { if let Some(value) = self.parsed.take() { self.value = value; cx.push(ValueMsg(value)); @@ -172,7 +173,7 @@ impl EditGuard for SpinGuard { } } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &A) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &A) { let is_err; if let Ok(value) = edit.as_str().parse::() { self.value = value.clamp(self.start, self.end); @@ -459,18 +460,19 @@ mod SpinBox { } else if let Some(DecrementStep) = cx.try_pop() { Some(self.edit.guard.value.sub_step(self.edit.guard.step)) } else if let Some(SetValueText(string)) = cx.try_pop() { - self.edit.set_string(cx, string); - self.edit.call_guard_edit(cx, data); + self.edit + .edit(cx, data, |edit, cx| edit.set_string(cx, string)); self.edit.guard.parsed } else if let Some(ReplaceSelectedText(text)) = cx.try_pop() { - self.edit.replace_selected_text(cx, &text); - self.edit.call_guard_edit(cx, data); + self.edit + .edit(cx, data, |edit, cx| edit.replace_selected_text(cx, &text)); self.edit.guard.parsed } else { None }; if let Some(value) = new_value { + self.edit.guard.value = value; if let Some(ref f) = self.on_change { f(cx, data, value); } diff --git a/crates/kas-widgets/src/text.rs b/crates/kas-widgets/src/text.rs index 680726fbd..eec723e90 100644 --- a/crates/kas-widgets/src/text.rs +++ b/crates/kas-widgets/src/text.rs @@ -6,8 +6,8 @@ //! Text widgets use kas::prelude::*; -use kas::text::format::FormattableText; -use kas::theme::{self, TextClass}; +use kas::text::{self, format::FormattableText}; +use kas::theme::TextClass; // NOTE: Text maintains a copy of the (formatted) string internally. Most // importantly this allows change detection. @@ -33,7 +33,7 @@ mod Text { #[layout(self.text)] pub struct Text { core: widget_core!(), - text: theme::Text, + text: text::Text, text_fn: Box bool + Send>, } @@ -44,7 +44,7 @@ mod Text { fn default() -> Self { Text { core: Default::default(), - text: theme::Text::new(T::default(), TextClass::Standard, true), + text: text::Text::new(T::default(), TextClass::Standard, true), text_fn: Box::new(|_, data, text| { let new_text = data.into(); let changed = new_text != *text; @@ -62,7 +62,7 @@ mod Text { pub fn new_str(as_str: impl Fn(&A) -> &str + Send + 'static) -> Self { Text { core: Default::default(), - text: theme::Text::new(String::new(), TextClass::Standard, true), + text: text::Text::new(String::new(), TextClass::Standard, true), text_fn: Box::new(move |_, data, text| { let s = as_str(data); let changed = *text != *s; @@ -83,7 +83,7 @@ mod Text { pub fn new_gen(gen_text: impl Fn(&ConfigCx, &A) -> T + Send + 'static) -> Self { Text { core: Default::default(), - text: theme::Text::new(T::default(), TextClass::Standard, true), + text: text::Text::new(T::default(), TextClass::Standard, true), text_fn: Box::new(move |cx, data, text| { let new_text = gen_text(cx, data); let changed = new_text != *text; @@ -108,7 +108,7 @@ mod Text { { Text { core: Default::default(), - text: theme::Text::new(T::default(), TextClass::Standard, true), + text: text::Text::new(T::default(), TextClass::Standard, true), text_fn: Box::new(update_text), } } @@ -159,7 +159,7 @@ mod Text { /// Get read access to the text object #[inline] - pub fn text(&self) -> &theme::Text { + pub fn text(&self) -> &text::Text { &self.text } diff --git a/examples/clock.rs b/examples/clock.rs index 3e16a0ef3..167a93602 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -21,7 +21,8 @@ use kas::draw::color::Rgba; use kas::event::TimerHandle; use kas::geom::{Quad, Vec2}; use kas::prelude::*; -use kas::theme::{Text, TextClass}; +use kas::text::Text; +use kas::theme::TextClass; const TIMER: TimerHandle = TimerHandle::new(0, true); @@ -110,8 +111,8 @@ mod Clock { line_seg(a_min, 0.0, half * 0.8, half * 0.02, col_hands); line_seg(a_sec, 0.0, half * 0.9, half * 0.01, col_secs); - cx.text_with_color(self.date.rect(), &self.date, col_date); - cx.text_with_color(self.time.rect(), &self.time, col_time); + self.date.draw_with_color(cx.re(), col_date); + self.time.draw_with_color(cx, col_time); } } diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs index 0de5c38c3..7b0159cd1 100644 --- a/examples/data-list-view.rs +++ b/examples/data-list-view.rs @@ -99,16 +99,16 @@ struct ListEntryGuard(usize); impl EditGuard for ListEntryGuard { type Data = MyItem; - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &MyItem) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &MyItem) { edit.set_string(cx, data.1.to_string()); } - fn activate(&mut self, _: &mut dyn Editor, cx: &mut EventCx, _: &MyItem) -> IsUsed { + fn activate(&mut self, _: &mut Editor, cx: &mut EventCx, _: &MyItem) -> IsUsed { cx.push(Control::Select(self.0)); Used } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &MyItem) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &MyItem) { cx.push(Control::Update(self.0, edit.clone_string())); } } diff --git a/examples/data-list.rs b/examples/data-list.rs index 7bd54344b..9cb3ab4b4 100644 --- a/examples/data-list.rs +++ b/examples/data-list.rs @@ -81,12 +81,12 @@ struct ListEntryGuard(usize); impl EditGuard for ListEntryGuard { type Data = Data; - fn activate(&mut self, _: &mut dyn Editor, cx: &mut EventCx, _: &Data) -> IsUsed { + fn activate(&mut self, _: &mut Editor, cx: &mut EventCx, _: &Data) -> IsUsed { cx.push(SelectEntry(self.0)); Used } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &Data) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Data) { if data.active == self.0 { cx.push(Control::UpdateCurrent(edit.clone_string())); } diff --git a/examples/file-explorer/src/tile.rs b/examples/file-explorer/src/tile.rs index 5fb3b9b44..9a68d4211 100644 --- a/examples/file-explorer/src/tile.rs +++ b/examples/file-explorer/src/tile.rs @@ -78,7 +78,7 @@ mod TextTile { #[layout(self.text)] pub struct TextTile { core: widget_core!(), - text: kas::theme::Text, + text: kas::text::Text, } impl Layout for Self { @@ -109,7 +109,7 @@ mod TextTile { Ok(TextTile { core: Default::default(), - text: kas::theme::Text::new(text, TextClass::Small, true), + text: kas::text::Text::new(text, TextClass::Small, true), }) } } diff --git a/examples/gallery.rs b/examples/gallery.rs index fcb51795a..7c0f98b7a 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -90,12 +90,12 @@ fn widgets() -> Page { impl EditGuard for Guard { type Data = Data; - fn activate(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &Data) -> IsUsed { + fn activate(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &Data) -> IsUsed { cx.push(Item::Edit(edit.clone_string())); Used } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &Data) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &Data) { // 7a is the colour of *magic*! if edit.as_str().len() % (7 + 1) == 0 { edit.set_error(cx, Some("Invalid length: is a multiple of (7 + 1)!".into())); @@ -121,7 +121,10 @@ fn widgets() -> Page { 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.set_text(cx, data.text.clone()); + self.popup.inner.edit(cx, |edit, cx| { + edit.clear(cx); + edit.set_string(cx, data.text.clone()); + }); // let ed = TextEdit::new(text, true); // cx.add_window::<()>(ed.into_window("Edit text")); // TODO: cx.add_modal(..) @@ -288,11 +291,11 @@ fn editor() -> Page { impl EditGuard for Guard { type Data = Data; - fn update(&mut self, edit: &mut dyn Editor, cx: &mut ConfigCx, data: &Data) { + fn update(&mut self, edit: &mut Editor, cx: &mut ConfigCx, data: &Data) { cx.set_disabled(edit.id(), data.disabled); } - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, data: &Data) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, data: &Data) { match Markdown::new(edit.as_str()) { Ok(text) => cx.send(data.label_id.clone(), text), Err(err) => edit.set_error(cx, Some(format!("{err}").into())), @@ -464,7 +467,7 @@ fn filter_list() -> Page { impl EditGuard for MonthYearFilterGuard { type Data = (); - fn edit(&mut self, edit: &mut dyn Editor, cx: &mut EventCx, _: &Self::Data) { + fn edit(&mut self, edit: &mut Editor, cx: &mut EventCx, _: &Self::Data) { let mut filter = MonthYearFilter { text: edit.as_str().to_uppercase(), month_end: edit.as_str().len(), diff --git a/examples/proxy.rs b/examples/proxy.rs index 73da2c58c..8f7fa2e8e 100644 --- a/examples/proxy.rs +++ b/examples/proxy.rs @@ -14,7 +14,8 @@ use std::time::{Duration, Instant}; use kas::draw::color::Rgba; use kas::prelude::*; -use kas::theme::{Text, TextClass}; +use kas::text::Text; +use kas::theme::TextClass; #[derive(Debug)] struct SetColor(Rgba); @@ -67,6 +68,7 @@ mod ColourSquare { } impl Layout for ColourSquare { fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules { + let _ = self.loading_text.size_rules(cx, axis); cx.logical(100.0, 100.0).build(axis) } @@ -80,7 +82,7 @@ mod ColourSquare { if let Some(color) = self.color { draw.draw().rect((self.rect()).cast(), color); } else { - draw.text(self.rect(), &self.loading_text); + self.loading_text.draw(draw); } } } diff --git a/examples/text-editor/text-editor.rs b/examples/text-editor/text-editor.rs index 59a8b24b1..2dbf5c0bf 100644 --- a/examples/text-editor/text-editor.rs +++ b/examples/text-editor/text-editor.rs @@ -6,8 +6,8 @@ //! A simple text editor use kas::prelude::*; +use kas::widgets::edit; use kas::widgets::edit::highlight::{SyntectHighlighter, SyntectSyntax}; -use kas::widgets::edit::{self, Editor as _}; use kas::widgets::{Button, EditBox, Filler, column, dialog, row}; use rfd::FileHandle; @@ -48,7 +48,7 @@ struct Guard { impl edit::EditGuard for Guard { type Data = (); - fn edit(&mut self, _: &mut dyn edit::Editor, _: &mut EventCx<'_>, _: &Self::Data) { + fn edit(&mut self, _: &mut edit::Editor, _: &mut EventCx<'_>, _: &Self::Data) { self.edited = true; } } @@ -131,9 +131,11 @@ mod Editor { } }; - self.editor.clear(cx); + self.editor.edit(cx, &(), |edit, cx| { + edit.clear(cx); + edit.set_string(cx, text); + }); self.editor.set_highlighter(SyntectHighlighter::new(syntax)); - self.editor.set_string(cx, text); self.editor.guard_mut().edited = false; } else if let Some(Saved(result)) = cx.try_pop() { match result { @@ -172,8 +174,10 @@ mod Editor { fn do_action(&mut self, cx: &mut EventCx<'_>, action: EditorAction) { match action { EditorAction::New => { - self.editor.clear(cx); - self.editor.set_string(cx, String::new()); + self.editor.edit(cx, &(), |edit, cx| { + edit.clear(cx); + edit.set_string(cx, String::new()); + }); self.editor.guard_mut().edited = false; self.file = None; }