diff --git a/Cargo.toml b/Cargo.toml index 294a53123..6e5a540c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,3 +162,7 @@ members = [ "crates/kas-view", "examples/mandlebrot", ] + +[patch.crates-io.kas-text] +git = "https://github.com/kas-gui/kas-text.git" +rev = "d9d2a99b7d94deeea48d67f3b7ba9237a12cea6d" diff --git a/crates/kas-core/src/decorations.rs b/crates/kas-core/src/decorations.rs index b3ef8e00f..7d0263d0e 100644 --- a/crates/kas-core/src/decorations.rs +++ b/crates/kas-core/src/decorations.rs @@ -8,7 +8,7 @@ //! Note: due to definition in kas-core, some widgets must be duplicated. use crate::event::{CursorIcon, ResizeDirection}; -use crate::text::Text; +use crate::text::{NotReady, Text}; use crate::theme::TextClass; use kas::prelude::*; use kas::theme::MarkStyle; @@ -125,15 +125,14 @@ impl_scope! { } impl Layout for Self { - #[inline] fn size_rules(&mut self, sizer: SizeCx, mut axis: AxisInfo) -> SizeRules { axis.set_default_align_hv(Align::Center, Align::Center); - sizer.text_rules(&mut self.label, Self::CLASS, axis) + sizer.text_rules(&mut self.label, axis) } fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.label, Self::CLASS, rect.size, None); + cx.text_set_size(&mut self.label, rect.size); } fn draw(&mut self, mut draw: DrawCx) { @@ -141,6 +140,12 @@ impl_scope! { } } + impl Events for Self { + fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.label, Self::CLASS); + } + } + impl HasStr for Self { fn get_str(&self) -> &str { self.label.as_str() @@ -150,9 +155,10 @@ impl_scope! { impl HasString for Self { fn set_string(&mut self, string: String) -> Action { self.label.set_string(string); - match self.label.try_prepare() { + match self.label.prepare() { + Err(NotReady) => Action::empty(), + Ok(false) => Action::REDRAW, Ok(true) => Action::RESIZE, - _ => Action::REDRAW, } } } diff --git a/crates/kas-core/src/event/cx/config.rs b/crates/kas-core/src/event/cx/config.rs index 2608f5c16..1b7ae44bc 100644 --- a/crates/kas-core/src/event/cx/config.rs +++ b/crates/kas-core/src/event/cx/config.rs @@ -13,6 +13,7 @@ use crate::messages::Erased; use crate::text::TextApi; use crate::theme::{Feature, SizeCx, TextClass, ThemeSize}; use crate::{Id, Node}; +use cast::Cast; use std::fmt::Debug; use std::ops::{Deref, DerefMut}; @@ -122,22 +123,26 @@ impl<'a> ConfigCx<'a> { self.sh.align_feature(feature, rect, align) } + /// Configure a text object + /// + /// This selects a font given the [`TextClass`], + /// [theme configuration][crate::theme::Config] and + /// the loaded [fonts][crate::text::fonts]. + #[inline] + pub fn text_configure(&self, text: &mut dyn TextApi, class: TextClass) { + self.sh.text_configure(text, class); + } + /// Prepare a text object /// - /// This sets the text's font, font size, wrapping and optionally alignment, - /// then performs the text preparation necessary before display. + /// Wrap and align text for display at the given `size`. /// - /// Note: setting alignment here is not necessary when the default alignment - /// is desired or when [`SizeCx::text_rules`] is used. + /// Call [`text_configure`][Self::text_configure] before this method. #[inline] - pub fn text_set_size( - &self, - text: &mut dyn TextApi, - class: TextClass, - size: Size, - align: Option, - ) { - self.sh.text_set_size(text, class, size, align) + pub fn text_set_size(&self, text: &mut dyn TextApi, size: Size) { + text.set_wrap_width(size.0.cast()); + text.set_bounds(size.cast()); + text.prepare().expect("not configured"); } } diff --git a/crates/kas-core/src/hidden.rs b/crates/kas-core/src/hidden.rs index 48cc3b589..c4fabc030 100644 --- a/crates/kas-core/src/hidden.rs +++ b/crates/kas-core/src/hidden.rs @@ -15,7 +15,7 @@ use crate::geom::{Coord, Offset, Rect}; use crate::layout::{Align, AxisInfo, SizeRules}; use crate::text::{Text, TextApi}; use crate::theme::{DrawCx, SizeCx, TextClass}; -use crate::{Id, Layout, NavAdvance, Node, Widget}; +use crate::{Events, Id, Layout, NavAdvance, Node, Widget}; use kas_macros::{autoimpl, impl_scope}; impl_scope! { @@ -51,12 +51,12 @@ impl_scope! { #[inline] fn size_rules(&mut self, sizer: SizeCx, mut axis: AxisInfo) -> SizeRules { axis.set_default_align_hv(Align::Default, Align::Center); - sizer.text_rules(&mut self.label, Self::CLASS, axis) + sizer.text_rules(&mut self.label, axis) } fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.label, Self::CLASS, rect.size, None); + cx.text_set_size(&mut self.label, rect.size); } fn draw(&mut self, mut draw: DrawCx) { @@ -64,6 +64,12 @@ impl_scope! { } } + impl Events for Self { + fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.label, Self::CLASS); + } + } + impl HasStr for Self { fn get_str(&self) -> &str { self.label.as_str() diff --git a/crates/kas-core/src/layout/mod.rs b/crates/kas-core/src/layout/mod.rs index 96d33b5ef..7398c94c5 100644 --- a/crates/kas-core/src/layout/mod.rs +++ b/crates/kas-core/src/layout/mod.rs @@ -168,16 +168,6 @@ impl AxisInfo { } } - /// Size of other axis, if fixed and `vertical` matches this axis. - #[inline] - pub fn size_other_if_fixed(&self, vertical: bool) -> Option { - if vertical == self.vertical && self.has_fixed { - Some(self.other_axis) - } else { - None - } - } - /// Subtract `x` from size of other axis (if applicable) #[inline] pub fn sub_other(&mut self, x: i32) { diff --git a/crates/kas-core/src/text/mod.rs b/crates/kas-core/src/text/mod.rs index ade422430..692fad3d5 100644 --- a/crates/kas-core/src/text/mod.rs +++ b/crates/kas-core/src/text/mod.rs @@ -8,10 +8,11 @@ //! 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. //! -//! [`Text`] objects *must* be prepared before usage, otherwise they may appear -//! empty. Call [`ConfigCx::text_set_size`] from [`Layout::set_rect`] to set -//! text position and prepare. If text is adjusted, one may use e.g. -//! [`TextApi::prepare`] to update. +//! [`Text`] objects *must* be configured and prepared before usage, otherwise +//! they may appear empty. Call [`ConfigCx::text_config`] from +//! [`Events::configure`] and [`ConfigCx::text_set_size`] from +//! [`Layout::set_rect`] to set text position and prepare. +//! If text is adjusted, one may use e.g. [`TextApi::prepare`] to update. //! //! [KAS Text]: https://github.com/kas-gui/kas-text/ diff --git a/crates/kas-core/src/theme/config.rs b/crates/kas-core/src/theme/config.rs index 3b5be046b..2d3891e12 100644 --- a/crates/kas-core/src/theme/config.rs +++ b/crates/kas-core/src/theme/config.rs @@ -6,7 +6,7 @@ //! Theme configuration use super::{ColorsSrgb, TextClass, ThemeConfig}; -use crate::text::fonts::{fonts, AddMode, FontSelector}; +use crate::text::fonts::{self, AddMode, FontSelector}; use crate::Action; use std::collections::BTreeMap; use std::time::Duration; @@ -244,7 +244,7 @@ impl ThemeConfig for Config { /// Apply config effects which only happen on startup fn apply_startup(&self) { if !self.font_aliases.is_empty() { - fonts().update_db(|db| { + fonts::library().update_db(|db| { for (family, aliases) in self.font_aliases.iter() { db.add_aliases( family.to_string().into(), diff --git a/crates/kas-core/src/theme/dimensions.rs b/crates/kas-core/src/theme/dimensions.rs index 52a80b4cc..5d777fe6f 100644 --- a/crates/kas-core/src/theme/dimensions.rs +++ b/crates/kas-core/src/theme/dimensions.rs @@ -16,7 +16,7 @@ use crate::cast::traits::*; use crate::dir::Directional; use crate::geom::{Rect, Size, Vec2}; use crate::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules, Stretch}; -use crate::text::{fonts::FontId, TextApi, TextApiExt}; +use crate::text::{fonts::FontId, Direction, TextApi}; crate::impl_scope! { /// Parameterisation of [`Dimensions`] @@ -319,50 +319,50 @@ impl ThemeSize for Window { fn line_height(&self, class: TextClass) -> i32 { let font_id = self.fonts.get(&class).cloned().unwrap_or_default(); - crate::text::fonts::fonts() + crate::text::fonts::library() .get_first_face(font_id) .expect("invalid font_id") .height(self.dims.dpem) .cast_ceil() } - fn text_rules(&self, text: &mut dyn TextApi, class: TextClass, axis: AxisInfo) -> SizeRules { + fn text_configure(&self, text: &mut dyn TextApi, class: TextClass) { + let direction = Direction::Auto; + let font_id = self.fonts.get(&class).cloned().unwrap_or_default(); + let dpem = self.dims.dpem; + let wrap = match class.multi_line() { + false => f32::INFINITY, + true => 0.0, // NOTE: finite value used as a flag + }; + text.set_font_properties(direction, font_id, dpem, wrap); + text.configure().expect("invalid font_id"); + } + + fn text_rules(&self, text: &mut dyn TextApi, axis: AxisInfo) -> SizeRules { let margin = match axis.is_horizontal() { true => self.dims.m_text.0, false => self.dims.m_text.1, }; let margins = (margin, margin); - let mut env = text.env(); - - // TODO(opt): maybe font look-up should only happen during configure? - if let Some(font_id) = self.fonts.get(&class).cloned() { - env.font_id = font_id; - } - env.dpem = self.dims.dpem; - // TODO(opt): setting horizontal alignment now could avoid re-wrapping - // text. Unfortunately we don't know the desired alignment here. - let wrap = class.multi_line(); - env.wrap = wrap; + let mut align_pair = text.get_align(); let align = axis.align_or_default(); if axis.is_horizontal() { - env.align.0 = align; + align_pair.0 = align; } else { - env.align.1 = align; - } - if let Some(size) = axis.size_other_if_fixed(true) { - env.bounds.0 = size.cast(); + align_pair.1 = align; } + text.set_align(align_pair); - text.set_env(env); + let wrap = text.get_wrap_width(); if axis.is_horizontal() { - if wrap { + if wrap.is_finite() { let min = self.dims.min_line_length; let limit = 2 * min; let bound: i32 = text .measure_width(limit.cast()) - .expect("invalid font_id") + .expect("not configured") .cast_ceil(); // NOTE: using different variable-width stretch policies here can @@ -372,36 +372,20 @@ impl ThemeSize for Window { } else { let bound: i32 = text .measure_width(f32::INFINITY) - .expect("invalid font_id") + .expect("not configured") .cast_ceil(); SizeRules::new(bound, bound, margins, Stretch::Filler) } } else { - let bound: i32 = text.measure_height().expect("invalid font_id").cast_ceil(); + if wrap.is_finite() { + text.set_wrap_width(axis.other().map(|w| w.cast()).unwrap_or(f32::INFINITY)); + } + + let bound: i32 = text.measure_height().expect("not configured").cast_ceil(); let line_height = self.dims.dpem.cast_ceil(); let min = bound.max(line_height); SizeRules::new(min, min, margins, Stretch::Filler) } } - - fn text_set_size( - &self, - text: &mut dyn TextApi, - class: TextClass, - size: Size, - align: Option, - ) { - let mut env = text.env(); - if let Some(font_id) = self.fonts.get(&class).cloned() { - env.font_id = font_id; - } - env.dpem = self.dims.dpem; - env.wrap = class.multi_line(); - if let Some(align) = align { - env.align = align.into(); - } - env.bounds = size.cast(); - text.update_env(env).expect("invalid font_id"); - } } diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 7d6cf2e55..86a8cf2b5 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -11,7 +11,7 @@ use crate::draw::color::Rgb; use crate::draw::{Draw, DrawIface, DrawShared, DrawSharedImpl, ImageId, PassType}; use crate::event::{ConfigCx, EventState}; use crate::geom::{Offset, Rect}; -use crate::text::{TextApi, TextDisplay}; +use crate::text::{format::FormattableText, Effect, Text, TextApi, TextApiExt, TextDisplay}; use crate::{autoimpl, Id, Layout}; use std::ops::{Bound, Range, RangeBounds}; use std::time::Instant; @@ -207,10 +207,17 @@ impl<'a> DrawCx<'a> { /// Text is drawn from `rect.pos` and clipped to `rect`. If the text /// scrolls, `rect` should be the size of the whole text, not the window. /// - /// [`ConfigCx::text_set_size`] should be called prior to this method to + /// [`ConfigCx::text_configure`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). - pub fn text(&mut self, rect: Rect, text: impl AsRef, class: TextClass) { - self.h.text(&self.id, rect, text.as_ref(), class); + pub fn text( + &mut self, + rect: Rect, + text: &Text, + class: TextClass, + ) { + if let Ok(display) = text.display() { + self.h.text(&self.id, rect, display, class); + } } /// Draw text with effects @@ -222,10 +229,18 @@ impl<'a> DrawCx<'a> { /// emphasis, text size. In addition, this method supports underline and /// strikethrough effects. /// - /// [`ConfigCx::text_set_size`] should be called prior to this method to + /// [`ConfigCx::text_configure`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). - pub fn text_effects(&mut self, rect: Rect, text: &dyn TextApi, class: TextClass) { - self.h.text_effects(&self.id, rect, text, class); + pub fn text_effects( + &mut self, + rect: Rect, + text: &Text, + class: TextClass, + ) { + let effects = text.effect_tokens(); + if let Ok(text) = text.display() { + self.h.text_effects(&self.id, rect, text, effects, class); + } } /// Draw some text using the standard font, with a subset selected @@ -233,13 +248,17 @@ impl<'a> DrawCx<'a> { /// Other than visually highlighting the selection, this method behaves /// identically to [`Self::text`]. It is likely to be replaced in the /// future by a higher-level API. - pub fn text_selected>( + pub fn text_selected>( &mut self, rect: Rect, - text: impl AsRef, + text: &Text, range: R, class: TextClass, ) { + let Ok(display) = text.display() else { + return; + }; + let start = match range.start_bound() { Bound::Included(n) => *n, Bound::Excluded(n) => *n + 1, @@ -252,7 +271,7 @@ impl<'a> DrawCx<'a> { }; let range = Range { start, end }; self.h - .text_selected_range(&self.id, rect, text.as_ref(), range, class); + .text_selected_range(&self.id, rect, display, range, class); } /// Draw an edit marker at the given `byte` index on this `text` @@ -260,17 +279,18 @@ impl<'a> DrawCx<'a> { /// The text cursor is draw from `rect.pos` and clipped to `rect`. If the text /// scrolls, `rect` should be the size of the whole text, not the window. /// - /// [`ConfigCx::text_set_size`] should be called prior to this method to + /// [`ConfigCx::text_configure`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). - pub fn text_cursor( + pub fn text_cursor( &mut self, rect: Rect, - text: impl AsRef, + text: &Text, class: TextClass, byte: usize, ) { - self.h - .text_cursor(&self.id, rect, text.as_ref(), class, byte); + if let Ok(text) = text.display() { + self.h.text_cursor(&self.id, rect, text, class, byte); + } } /// Draw UI element: check box (without label) @@ -409,7 +429,7 @@ pub trait ThemeDraw { /// Draw text /// - /// [`ConfigCx::text_set_size`] should be called prior to this method to + /// [`ConfigCx::text_configure`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). fn text(&mut self, id: &Id, rect: Rect, text: &TextDisplay, class: TextClass); @@ -419,9 +439,16 @@ pub trait ThemeDraw { /// emphasis, text size. In addition, this method supports underline and /// strikethrough effects. /// - /// [`ConfigCx::text_set_size`] should be called prior to this method to + /// [`ConfigCx::text_configure`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). - fn text_effects(&mut self, id: &Id, rect: Rect, text: &dyn TextApi, class: TextClass); + fn text_effects( + &mut self, + id: &Id, + rect: Rect, + text: &TextDisplay, + effects: &[Effect<()>], + class: TextClass, + ); /// Method used to implement [`DrawCx::text_selected`] fn text_selected_range( @@ -435,7 +462,7 @@ pub trait ThemeDraw { /// Draw an edit marker at the given `byte` index on this `text` /// - /// [`ConfigCx::text_set_size`] should be called prior to this method to + /// [`ConfigCx::text_configure`] should be called prior to this method to /// select a font, font size and wrap options (based on the [`TextClass`]). fn text_cursor( &mut self, diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index 9fa992360..8db9adf70 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -15,7 +15,7 @@ use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::event::EventState; use kas::geom::*; -use kas::text::{TextApi, TextDisplay}; +use kas::text::TextDisplay; use kas::theme::dimensions as dim; use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; use kas::theme::{ColorsLinear, Config, InputState, Theme}; diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index b0e1a715a..ab770449d 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -16,7 +16,7 @@ use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::event::EventState; use kas::geom::*; -use kas::text::{fonts, Effect, TextApi, TextDisplay}; +use kas::text::{fonts, Effect, TextDisplay}; use kas::theme::dimensions as dim; use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; use kas::theme::{ColorsLinear, Config, InputState, Theme}; @@ -105,7 +105,7 @@ where } fn init(&mut self, _shared: &mut SharedState) { - let fonts = fonts::fonts(); + let fonts = fonts::library(); if let Err(e) = fonts.select_default() { panic!("Error loading font: {e}"); } @@ -396,17 +396,23 @@ where self.draw.text(rect, text, col); } - fn text_effects(&mut self, id: &Id, rect: Rect, text: &dyn TextApi, class: TextClass) { + fn text_effects( + &mut self, + id: &Id, + rect: Rect, + text: &TextDisplay, + effects: &[Effect<()>], + class: TextClass, + ) { let col = if self.ev.is_disabled(id) { self.cols.text_disabled } else { self.cols.text }; if class.is_access_key() && !self.ev.show_access_labels() { - self.draw.text(rect, text.display(), col); + self.draw.text(rect, text, col); } else { - self.draw - .text_effects(rect, text.display(), col, text.effect_tokens()); + self.draw.text_effects(rect, text, col, effects); } } @@ -426,7 +432,7 @@ where let sel_col = self.cols.text_over(self.cols.text_sel_bg); // Draw background: - let result = text.highlight_range(range.clone(), &mut |p1, p2| { + text.highlight_range(range.clone(), &mut |p1, p2| { let q = Quad::conv(rect); let p1 = Vec2::from(p1); let p2 = Vec2::from(p2); @@ -434,9 +440,6 @@ where self.draw.rect(quad, self.cols.text_sel_bg); } }); - if let Err(e) = result { - log::error!("text_selected_range: text.highlight_range() -> {e}"); - } let effects = [ Effect { @@ -468,7 +471,7 @@ where let p10max = pos.0 + f32::conv(rect.size.0) - width; let mut col = self.cols.nav_focus; - for cursor in text.text_glyph_pos(byte).iter_mut().flatten().rev() { + for cursor in text.text_glyph_pos(byte).rev() { let mut p1 = pos + Vec2::from(cursor.pos); p1.0 = p1.0.min(p10max); let mut p2 = p1; diff --git a/crates/kas-core/src/theme/size.rs b/crates/kas-core/src/theme/size.rs index d4e0d5792..094e07172 100644 --- a/crates/kas-core/src/theme/size.rs +++ b/crates/kas-core/src/theme/size.rs @@ -5,14 +5,15 @@ //! "Handle" types used by themes -use std::ops::Deref; +use cast::CastFloat; use super::{Feature, FrameStyle, MarginStyle, TextClass}; use crate::autoimpl; use crate::dir::Directional; -use crate::geom::{Rect, Size}; +use crate::geom::Rect; use crate::layout::{AlignPair, AxisInfo, FrameRules, Margins, SizeRules}; use crate::text::TextApi; +use std::ops::Deref; #[allow(unused)] use crate::text::TextApiExt; #[allow(unused)] @@ -155,10 +156,17 @@ impl<'a> SizeCx<'a> { /// scales this according to [`Self::dpem`], then measures the line height. /// The result is typically 100% - 150% of the value returned by /// [`Self::dpem`], depending on the font face. + /// + /// Prefer to use [`Self::text_line_height`] where possible. pub fn line_height(&self, class: TextClass) -> i32 { self.0.line_height(class) } + /// Get the line-height of a configured text object + pub fn text_line_height(&self, text: &dyn TextApi) -> i32 { + text.line_height().expect("not configured").cast_ceil() + } + /// Get [`SizeRules`] for a text element /// /// The [`TextClass`] is used to select a font and controls whether line @@ -179,14 +187,9 @@ impl<'a> SizeCx<'a> { /// /// Note: this method partially prepares the `text` object. It is not /// required to call this method but it is required to call - /// [`ConfigCx::text_set_size`] before text display for correct results. - pub fn text_rules( - &self, - text: &mut dyn TextApi, - class: TextClass, - axis: AxisInfo, - ) -> SizeRules { - self.0.text_rules(text, class, axis) + /// [`ConfigCx::text_configure`] before text display for correct results. + pub fn text_rules(&self, text: &mut dyn TextApi, axis: AxisInfo) -> SizeRules { + self.0.text_rules(text, axis) } } @@ -225,18 +228,14 @@ pub trait ThemeSize { /// Size of a frame around another element fn frame(&self, style: FrameStyle, axis_is_vertical: bool) -> FrameRules; - /// The height of a line of text using the standard font + /// The height of a line of text by class + /// + /// Prefer to use [`Self::text_line_height`] where possible. fn line_height(&self, class: TextClass) -> i32; + /// Configure a text object, setting font properties + fn text_configure(&self, text: &mut dyn TextApi, class: TextClass); + /// Get [`SizeRules`] for a text element - fn text_rules(&self, text: &mut dyn TextApi, class: TextClass, axis: AxisInfo) -> SizeRules; - - /// Update a text object, setting font properties and wrap size - fn text_set_size( - &self, - text: &mut dyn TextApi, - class: TextClass, - size: Size, - align: Option, - ); + fn text_rules(&self, text: &mut dyn TextApi, axis: AxisInfo) -> SizeRules; } diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index b5f9bd55c..6d15ca1d0 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -80,8 +80,8 @@ impl Extends { (#base).text(id, rect, text, class); } - fn text_effects(&mut self, id: &Id, rect: Rect, text: &dyn TextApi, class: TextClass) { - (#base).text_effects(id, rect, text, class); + fn text_effects(&mut self, id: &Id, rect: Rect, text: &TextDisplay, effects: &[::kas::text::Effect<()>], class: TextClass) { + (#base).text_effects(id, rect, text, effects, class); } fn text_selected_range( diff --git a/crates/kas-resvg/src/svg.rs b/crates/kas-resvg/src/svg.rs index 01f5088a2..9dc0b302c 100644 --- a/crates/kas-resvg/src/svg.rs +++ b/crates/kas-resvg/src/svg.rs @@ -26,7 +26,7 @@ enum LoadError { fn load(data: &[u8], resources_dir: Option<&Path>) -> Result { use once_cell::sync::Lazy; static FONT_FAMILY: Lazy = Lazy::new(|| { - let fonts_db = kas::text::fonts::fonts().read_db(); + let fonts_db = kas::text::fonts::library().read_db(); fonts_db.font_family_from_alias("SERIF").unwrap_or_default() }); diff --git a/crates/kas-wgpu/src/draw/text_pipe.rs b/crates/kas-wgpu/src/draw/text_pipe.rs index 90de0d9be..b304fa4dc 100644 --- a/crates/kas-wgpu/src/draw/text_pipe.rs +++ b/crates/kas-wgpu/src/draw/text_pipe.rs @@ -272,9 +272,7 @@ impl Window { } } }; - if let Err(e) = text.glyphs(for_glyph) { - log::warn!("Window: display failed: {e}"); - } + text.glyphs(for_glyph); } #[allow(clippy::too_many_arguments)] @@ -311,7 +309,7 @@ impl Window { } }; - let result = if effects.len() > 1 + if effects.len() > 1 || effects .first() .map(|e| *e != Effect::default(())) @@ -330,10 +328,6 @@ impl Window { } else { text.glyphs(|face, dpem, glyph| for_glyph(face, dpem, glyph, 0, ())) }; - - if let Err(e) = result { - log::warn!("Window: display failed: {e}"); - } } pub fn text_effects_rgba( @@ -379,8 +373,6 @@ impl Window { } }; - if let Err(e) = text.glyphs_with_effects(effects, Rgba::BLACK, for_glyph, for_rect) { - log::warn!("Window: display failed: {e}"); - } + text.glyphs_with_effects(effects, Rgba::BLACK, for_glyph, for_rect) } } diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index 156cd01cb..cf3886b56 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -15,7 +15,7 @@ use kas::dir::{Direction, Directional}; use kas::draw::{color::Rgba, *}; use kas::event::EventState; use kas::geom::*; -use kas::text::{TextApi, TextDisplay}; +use kas::text::TextDisplay; use kas::theme::dimensions as dim; use kas::theme::{Background, ThemeControl, ThemeDraw, ThemeSize}; use kas::theme::{ColorsLinear, Config, FlatTheme, InputState, SimpleTheme, Theme}; diff --git a/crates/kas-widgets/src/check_box.rs b/crates/kas-widgets/src/check_box.rs index e8d686987..f0c5b335b 100644 --- a/crates/kas-widgets/src/check_box.rs +++ b/crates/kas-widgets/src/check_box.rs @@ -285,8 +285,8 @@ impl_scope! { fn direction(&self) -> Direction { match self.label.text().text_is_rtl() { - Ok(false) | Err(_) => Direction::Right, - Ok(true) => Direction::Left, + false => Direction::Right, + true => Direction::Left, } } } diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index ee0b276b8..83c79642f 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -656,7 +656,6 @@ impl_scope! { view_offset: Offset, editable: bool, class: TextClass = TextClass::Edit(false), - align: AlignPair, width: (f32, f32) = (8.0, 16.0), lines: (i32, i32) = (1, 1), text: Text, @@ -674,27 +673,34 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules { + let mut align = self.text.get_align(); let (min, ideal) = if axis.is_horizontal() { + align.0 = axis.align_or_default(); let dpem = sizer.dpem(); ((self.width.0 * dpem).cast_ceil(), (self.width.1 * dpem).cast_ceil()) } else { + align.1 = if self.multi_line() { + axis.align_or_default() + } else { + axis.align_or_center() + }; let height = sizer.line_height(self.class); (self.lines.0 * height, self.lines.1 * height) }; + self.text.set_align(align.into()); let margins = sizer.text_margins().extract(axis); - let (stretch, align) = if axis.is_horizontal() || self.multi_line() { - (Stretch::High, axis.align_or_default()) + let stretch = if axis.is_horizontal() || self.multi_line() { + Stretch::High } else { - (Stretch::None, axis.align_or_center()) + Stretch::None }; - self.align.set_component(axis, align); SizeRules::new(min, ideal, margins, stretch) } fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; self.outer_rect = rect; - cx.text_set_size(&mut self.text, self.class, rect.size, Some(self.align)); + cx.text_set_size(&mut self.text, rect.size); self.text_size = Vec2::from(self.text.bounding_box().unwrap().1).cast_ceil(); self.view_offset = self.view_offset.min(self.max_scroll_offset()); } @@ -743,6 +749,7 @@ impl_scope! { type Data = G::Data; fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.text, self.class); G::configure(self, cx); } @@ -918,7 +925,7 @@ impl_scope! { let len = string.len(); self.text.set_string(string); self.selection.set_max_len(len); - if self.text.try_prepare().is_ok() { + if self.text.prepare().is_ok() { self.text_size = Vec2::from(self.text.bounding_box().unwrap().1).cast_ceil(); let view_offset = self.view_offset.min(self.max_scroll_offset()); if view_offset != self.view_offset { @@ -948,7 +955,6 @@ impl EditField { view_offset: Default::default(), editable: true, class: TextClass::Edit(false), - align: Default::default(), width: (8.0, 16.0), lines: (1, 1), text: Default::default(), @@ -1183,12 +1189,12 @@ impl EditField { } fn prepare_text(&mut self, cx: &mut EventCx) { - if !self.text.env().bounds.1.is_finite() { + if !self.text.get_bounds().1.is_finite() { // Do not attempt to prepare before bounds are set. return; } - if !self.text.required_action().is_ready() { + if !self.text.is_prepared() { let start = std::time::Instant::now(); self.text.prepare().expect("invalid font_id"); @@ -1397,7 +1403,7 @@ impl EditField { v.0 = x; } const FACTOR: f32 = 2.0 / 3.0; - let mut h_dist = self.text.env().bounds.1 * FACTOR; + let mut h_dist = self.text.get_bounds().1 * FACTOR; if cmd == Command::PageUp { h_dist *= -1.0; } @@ -1596,7 +1602,7 @@ impl EditField { .ok() .and_then(|mut m| m.next_back()) { - let bounds = Vec2::from(self.text.env().bounds); + let bounds = Vec2::from(self.text.get_bounds()); let min_x = marker.pos.0 - bounds.0; let min_y = marker.pos.1 - marker.descent - bounds.1; let max_x = marker.pos.0; diff --git a/crates/kas-widgets/src/label.rs b/crates/kas-widgets/src/label.rs index ccc30074a..595b1f570 100644 --- a/crates/kas-widgets/src/label.rs +++ b/crates/kas-widgets/src/label.rs @@ -8,7 +8,7 @@ use super::adapt::MapAny; use kas::prelude::*; use kas::text::format::{EditableText, FormattableText}; -use kas::text::Text; +use kas::text::{NotReady, Text}; use kas::theme::TextClass; /// Construct a [`Label`] @@ -116,9 +116,11 @@ impl_scope! { /// Note: this must not be called before fonts have been initialised /// (usually done by the theme when the main loop starts). pub fn set_text(&mut self, text: T) -> Action { - match self.label.set_and_try_prepare(text) { + self.label.set_text(text); + match self.label.prepare() { + Err(NotReady) => Action::empty(), + Ok(false) => Action::REDRAW, Ok(true) => Action::RESIZE, - _ => Action::REDRAW, } } } @@ -127,12 +129,12 @@ impl_scope! { #[inline] fn size_rules(&mut self, sizer: SizeCx, mut axis: AxisInfo) -> SizeRules { axis.set_default_align_hv(Align::Default, Align::Center); - sizer.text_rules(&mut self.label, self.class, axis) + sizer.text_rules(&mut self.label, axis) } fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.label, self.class, rect.size, None); + cx.text_set_size(&mut self.label, rect.size); } #[cfg(feature = "min_spec")] @@ -145,6 +147,12 @@ impl_scope! { } } + impl Events for Self { + fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.label, self.class); + } + } + impl HasStr for Self { fn get_str(&self) -> &str { self.label.as_str() @@ -157,9 +165,10 @@ impl_scope! { { fn set_string(&mut self, string: String) -> Action { self.label.set_string(string); - match self.label.try_prepare() { + match self.label.prepare() { + Err(NotReady) => Action::empty(), + Ok(false) => Action::REDRAW, Ok(true) => Action::RESIZE, - _ => Action::REDRAW, } } } @@ -290,9 +299,11 @@ impl_scope! { /// Note: this must not be called before fonts have been initialised /// (usually done by the theme when the main loop starts). pub fn set_text(&mut self, text: AccessString) -> Action { - match self.label.set_and_try_prepare(text) { + self.label.set_text(text); + match self.label.prepare() { + Err(NotReady) => Action::empty(), + Ok(false) => Action::REDRAW, Ok(true) => Action::RESIZE, - _ => Action::REDRAW, } } } @@ -301,12 +312,12 @@ impl_scope! { #[inline] fn size_rules(&mut self, sizer: SizeCx, mut axis: AxisInfo) -> SizeRules { axis.set_default_align_hv(Align::Default, Align::Center); - sizer.text_rules(&mut self.label, self.class, axis) + sizer.text_rules(&mut self.label, axis) } fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.label, self.class, rect.size, None); + cx.text_set_size(&mut self.label, rect.size); } fn draw(&mut self, mut draw: DrawCx) { @@ -318,6 +329,8 @@ impl_scope! { type Data = (); fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.label, self.class); + if let Some(key) = self.label.text().key() { cx.add_access_key(self.id_ref(), key.clone()); } diff --git a/crates/kas-widgets/src/radio_box.rs b/crates/kas-widgets/src/radio_box.rs index 3b4e0dd85..b417ab79e 100644 --- a/crates/kas-widgets/src/radio_box.rs +++ b/crates/kas-widgets/src/radio_box.rs @@ -233,8 +233,8 @@ impl_scope! { fn direction(&self) -> Direction { match self.label.text().text_is_rtl() { - Ok(false) | Err(_) => Direction::Right, - Ok(true) => Direction::Left, + false => Direction::Right, + true => Direction::Left, } } } diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index 67adbcd5a..7d9db0afa 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -11,7 +11,7 @@ use kas::event::{Command, CursorIcon, FocusSource, Scroll, ScrollDelta}; use kas::geom::Vec2; use kas::prelude::*; use kas::text::format::{EditableText, FormattableText}; -use kas::text::{SelectionHelper, Text}; +use kas::text::{NotReady, SelectionHelper, Text}; use kas::theme::TextClass; impl_scope! { @@ -37,18 +37,17 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules { - let class = TextClass::LabelScroll; - let mut rules = sizer.text_rules(&mut self.text, class, axis); + let mut rules = sizer.text_rules(&mut self.text, axis); let _ = self.bar.size_rules(sizer.re(), axis); if axis.is_vertical() { - rules.reduce_min_to(sizer.line_height(class) * 4); + rules.reduce_min_to(sizer.text_line_height(&self.text) * 4); } rules } fn set_rect(&mut self, cx: &mut ConfigCx, mut rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.text, TextClass::LabelScroll, rect.size, None); + cx.text_set_size(&mut self.text, rect.size); self.text_size = Vec2::from(self.text.bounding_box().unwrap().1).cast_ceil(); let max_offset = self.max_scroll_offset(); @@ -110,9 +109,10 @@ impl_scope! { /// Note: this must not be called before fonts have been initialised /// (usually done by the theme when the main loop starts). pub fn set_text(&mut self, text: T) -> Action { - self.text - .set_and_try_prepare(text) - .expect("invalid font_id"); + self.text.set_text(text); + if self.text.prepare().is_err() { + return Action::empty(); + } self.text_size = Vec2::from(self.text.bounding_box().unwrap().1).cast_ceil(); let max_offset = self.max_scroll_offset(); @@ -171,7 +171,7 @@ impl_scope! { .ok() .and_then(|mut m| m.next_back()) { - let bounds = Vec2::from(self.text.env().bounds); + let bounds = Vec2::from(self.text.get_bounds()); let min_x = marker.pos.0 - bounds.0; let min_y = marker.pos.1 - marker.descent - bounds.1; let max_x = marker.pos.0; @@ -209,14 +209,21 @@ impl_scope! { { fn set_string(&mut self, string: String) -> Action { self.text.set_string(string); - let _ = self.text.try_prepare(); - Action::REDRAW + match self.text.prepare() { + Err(NotReady) => Action::empty(), + Ok(false) => Action::REDRAW, + Ok(true) => Action::SET_RECT, + } } } impl Events for Self { type Data = (); + fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.text, TextClass::LabelScroll); + } + fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed { match event { Event::Command(cmd, _) => match cmd { diff --git a/crates/kas-widgets/src/scroll_text.rs b/crates/kas-widgets/src/scroll_text.rs index 3a228cb11..f67673ce2 100644 --- a/crates/kas-widgets/src/scroll_text.rs +++ b/crates/kas-widgets/src/scroll_text.rs @@ -11,7 +11,7 @@ use kas::event::{Command, CursorIcon, FocusSource, Scroll, ScrollDelta}; use kas::geom::Vec2; use kas::prelude::*; use kas::text::format::{EditableText, FormattableText}; -use kas::text::{SelectionHelper, Text}; +use kas::text::{NotReady, SelectionHelper, Text}; use kas::theme::TextClass; impl_scope! { @@ -37,18 +37,17 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules { - let class = TextClass::LabelScroll; - let mut rules = sizer.text_rules(&mut self.text, class, axis); + let mut rules = sizer.text_rules(&mut self.text, axis); let _ = self.bar.size_rules(sizer.re(), axis); if axis.is_vertical() { - rules.reduce_min_to(sizer.line_height(class) * 4); + rules.reduce_min_to(sizer.text_line_height(&self.text) * 4); } rules } fn set_rect(&mut self, cx: &mut ConfigCx, mut rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.text, TextClass::LabelScroll, rect.size, None); + cx.text_set_size(&mut self.text, rect.size); self.text_size = Vec2::from(self.text.bounding_box().unwrap().1).cast_ceil(); let max_offset = self.max_scroll_offset(); @@ -153,7 +152,7 @@ impl_scope! { .ok() .and_then(|mut m| m.next_back()) { - let bounds = Vec2::from(self.text.env().bounds); + let bounds = Vec2::from(self.text.get_bounds()); let min_x = marker.pos.0 - bounds.0; let min_y = marker.pos.1 - marker.descent - bounds.1; let max_x = marker.pos.0; @@ -191,14 +190,21 @@ impl_scope! { { fn set_string(&mut self, string: String) -> Action { self.text.set_string(string); - let _ = self.text.try_prepare(); - Action::REDRAW + match self.text.prepare() { + Err(NotReady) => Action::empty(), + Ok(false) => Action::REDRAW, + Ok(true) => Action::SET_RECT, + } } } impl Events for Self { type Data = A; + fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.text, TextClass::LabelScroll); + } + fn update(&mut self, cx: &mut ConfigCx, data: &A) { let text = (self.text_fn)(cx, data); if text.as_str() == self.text.as_str() { @@ -207,12 +213,16 @@ impl_scope! { return; } self.text.set_text(text); - if self.text.env().bounds.1.is_finite() { + if self.text.get_bounds().1.is_finite() { // NOTE: bounds are initially infinite. Alignment results in // infinite offset and thus infinite measured height. - let action = match self.text.try_prepare() { - Ok(true) => Action::RESIZE, - _ => Action::REDRAW, + let action = match self.text.prepare() { + Err(NotReady) => { + debug_assert!(false, "update before configure"); + Action::empty() + } + Ok(false) => Action::REDRAW, + Ok(true) => Action::SET_RECT, }; cx.action(self, action); } diff --git a/crates/kas-widgets/src/text.rs b/crates/kas-widgets/src/text.rs index 1b0705794..6d2edc943 100644 --- a/crates/kas-widgets/src/text.rs +++ b/crates/kas-widgets/src/text.rs @@ -8,6 +8,7 @@ use kas::prelude::*; use kas::text; use kas::text::format::FormattableText; +use kas::text::NotReady; use kas::theme::TextClass; impl_scope! { @@ -111,12 +112,12 @@ impl_scope! { #[inline] fn size_rules(&mut self, sizer: SizeCx, mut axis: AxisInfo) -> SizeRules { axis.set_default_align_hv(Align::Default, Align::Center); - sizer.text_rules(&mut self.label, self.class, axis) + sizer.text_rules(&mut self.label, axis) } fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; - cx.text_set_size(&mut self.label, self.class, rect.size, None); + cx.text_set_size(&mut self.label, rect.size); } #[cfg(feature = "min_spec")] @@ -132,6 +133,10 @@ impl_scope! { impl Events for Self { type Data = A; + fn configure(&mut self, cx: &mut ConfigCx) { + cx.text_configure(&mut self.label, self.class); + } + fn update(&mut self, cx: &mut ConfigCx, data: &A) { let text = (self.label_fn)(cx, data); if text.as_str() == self.label.as_str() { @@ -140,12 +145,16 @@ impl_scope! { return; } self.label.set_text(text); - if self.label.env().bounds.1.is_finite() { + if self.label.get_bounds().1.is_finite() { // NOTE: bounds are initially infinite. Alignment results in // infinite offset and thus infinite measured height. - let action = match self.label.try_prepare() { + let action = match self.label.prepare() { + Err(NotReady) => { + debug_assert!(false, "update before configure"); + Action::empty() + } + Ok(false) => Action::REDRAW, Ok(true) => Action::RESIZE, - _ => Action::REDRAW, }; cx.action(self, action); } diff --git a/examples/clock.rs b/examples/clock.rs index 90e53a93b..51b6f88dc 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -58,12 +58,10 @@ impl_scope! { let text_size = Size(size.0, size.1 / 4); let text_height = text_size.1 as f32; - let mut env = self.date.env(); - env.dpem = text_height * 0.5; - env.bounds = text_size.cast(); - self.date.update_env(env).expect("invalid font_id"); - env.dpem = text_height * 0.7; - self.time.update_env(env).expect("invalid font_id"); + self.date.set_font_size(text_height * 0.5); + self.date.set_bounds(text_size.cast()); + self.time.set_font_size(text_height * 0.7); + self.time.set_bounds(text_size.cast()); let time_pos = pos + Offset(0, size.1 * 5 / 8); let date_pos = pos + Offset(0, size.1 / 8); @@ -102,8 +100,12 @@ impl_scope! { draw.rounded_line(centre + v * (r - l), centre + v * r, w, col_face); } - draw.text(self.date_rect, self.date.as_ref(), col_date); - draw.text(self.time_rect, self.time.as_ref(), col_time); + if let Ok(text) = self.date.display() { + draw.text(self.date_rect, text, col_date); + } + if let Ok(text) = self.time.display() { + draw.text(self.time_rect, text, col_time); + } let mut line_seg = |t: f32, r1: f32, r2: f32, w, col| { let v = Vec2(t.sin(), -t.cos()); @@ -125,6 +127,10 @@ impl_scope! { type Data = (); fn configure(&mut self, cx: &mut ConfigCx) { + self.date.set_align(AlignPair::CENTER.into()); + self.date.configure().unwrap(); + self.time.set_align(AlignPair::CENTER.into()); + self.time.configure().unwrap(); cx.request_timer(self.id(), 0, Duration::new(0, 0)); } @@ -134,12 +140,10 @@ impl_scope! { self.now = Local::now(); let date = self.now.format("%Y-%m-%d").to_string(); let time = self.now.format("%H:%M:%S").to_string(); - self.date - .set_and_try_prepare(date) - .expect("invalid font_id"); - self.time - .set_and_try_prepare(time) - .expect("invalid font_id"); + self.date.set_text(date); + self.date.prepare().expect("not configured"); + self.time.set_text(time); + self.time.prepare().expect("not configured"); let ns = 1_000_000_000 - (self.now.time().nanosecond() % 1_000_000_000); log::info!("Requesting update in {}ns", ns); cx.request_timer(self.id(), 0, Duration::new(0, ns)); @@ -153,19 +157,13 @@ impl_scope! { impl Clock { fn new() -> Self { - let env = kas::text::Environment { - align: (Align::Center, Align::Center), - ..Default::default() - }; - let date = Text::new_env(env, "0000-00-00".into()); - let time = Text::new_env(env, "00:00:00".into()); Clock { core: Default::default(), date_rect: Rect::ZERO, time_rect: Rect::ZERO, now: Local::now(), - date, - time, + date: Text::new("0000-00-00".to_string()), + time: Text::new("00:00:00".to_string()), } } } diff --git a/examples/proxy.rs b/examples/proxy.rs index 5a2e14315..020c8d836 100644 --- a/examples/proxy.rs +++ b/examples/proxy.rs @@ -72,13 +72,7 @@ impl_scope! { fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect) { self.core.rect = rect; - let align = Some(AlignPair::new(Align::Center, Align::Center)); - cx.text_set_size( - &mut self.loading_text, - TextClass::Label(false), - rect.size, - align, - ); + cx.text_set_size(&mut self.loading_text, rect.size); } fn draw(&mut self, mut draw: DrawCx) { @@ -93,6 +87,11 @@ impl_scope! { impl Events for ColourSquare { type Data = AppData; + fn configure(&mut self, cx: &mut ConfigCx) { + self.loading_text.set_align((Align::Center, Align::Center)); + cx.text_configure(&mut self.loading_text, TextClass::Label(false)); + } + fn update(&mut self, cx: &mut ConfigCx, data: &AppData) { self.color = data.color; cx.redraw(self);