From 3890bd093a66205b16e908d9a3e86e58565913af Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 17 Feb 2026 10:28:54 +0000 Subject: [PATCH 01/14] kas-text update: extract Effect::start --- Cargo.toml | 2 +- crates/kas-core/src/draw/draw.rs | 4 ++-- crates/kas-core/src/draw/draw_shared.rs | 2 +- crates/kas-core/src/text/raster.rs | 4 ++-- crates/kas-core/src/text/string.rs | 16 +++++++--------- crates/kas-core/src/theme/draw.rs | 10 +++++----- crates/kas-core/src/theme/simple_theme.rs | 17 +++++++---------- crates/kas-core/src/theme/text.rs | 2 +- crates/kas-macros/src/extends.rs | 2 +- crates/kas-soft/src/draw.rs | 2 +- crates/kas-wgpu/src/draw/draw_pipe.rs | 2 +- crates/kas-widgets/src/edit/editor.rs | 15 ++++++--------- 12 files changed, 35 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f26a129a6..63382eb57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,4 +172,4 @@ resolver = "2" [patch.crates-io.kas-text] git = "https://github.com/kas-gui/kas-text.git" -rev = "f8d88435996ccacebc4893793dc47580be2251b7" +rev = "62a35caa" diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index 2421d81c2..6ddeefdd9 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -241,7 +241,7 @@ pub trait Draw { bounding_box: Quad, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ); } @@ -299,7 +299,7 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { bb: Quad, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ) { self.shared .draw diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index aba1e1ae2..554c631c2 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -216,6 +216,6 @@ pub trait DrawSharedImpl: Any { bb: Quad, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ); } diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index d22ba7709..fc30a7704 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -543,14 +543,14 @@ impl State { bb: Quad, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], mut draw_quad: impl FnMut(Quad, Rgba), ) { // Optimisation: use cheaper TextDisplay::runs method if effects.len() <= 1 && effects .first() - .map(|e| e.flags == Default::default()) + .map(|e| e.1.flags == Default::default()) .unwrap_or(true) { let col = colors.first().cloned().unwrap_or(Rgba::BLACK); diff --git a/crates/kas-core/src/text/string.rs b/crates/kas-core/src/text/string.rs index 9cd50d378..0a5f31f76 100644 --- a/crates/kas-core/src/text/string.rs +++ b/crates/kas-core/src/text/string.rs @@ -29,7 +29,7 @@ use crate::text::{Effect, EffectFlags, fonts::FontSelector}; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct AccessString { text: String, - key: Option<(Key, [Effect; 2])>, + key: Option<(Key, [(u32, Effect); 2])>, } impl AccessString { @@ -60,20 +60,18 @@ impl AccessString { let k = c.to_ascii_lowercase().encode_utf8(&mut kbuf); let k = Key::Character(k.into()); - let e0 = Effect { - start, + let e0 = (start, Effect { color: 0, flags: EffectFlags::UNDERLINE, - }; + }); let i = c.len_utf8(); s = &s[i..]; - let e1 = Effect { - start: start + u32::conv(i), + let e1 = (start + u32::conv(i), Effect { color: 0, flags: EffectFlags::empty(), - }; + }); key = Some((k, [e0, e1])); } @@ -90,7 +88,7 @@ impl AccessString { } /// Get the key bindings and associated effects, if any - pub fn key(&self) -> Option<&(Key, [Effect; 2])> { + pub fn key(&self) -> Option<&(Key, [(u32, Effect); 2])> { self.key.as_ref() } @@ -111,7 +109,7 @@ impl FormattableText for AccessString { std::iter::empty() } - fn effect_tokens(&self) -> &[Effect] { + fn effect_tokens(&self) -> &[(u32, Effect)] { &[] } } diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index a4658d947..7d279ffce 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -343,16 +343,16 @@ impl<'a> DrawCx<'a> { rect: Rect, display: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ) { if cfg!(debug_assertions) { let num_colors = if colors.is_empty() { 1 } else { colors.len() }; let mut i = 0; for effect in effects { - assert!(effect.start >= i); - i = effect.start; + assert!(effect.0 >= i); + i = effect.0; - assert!(usize::from(effect.color) < num_colors); + assert!(usize::from(effect.1.color) < num_colors); } } @@ -559,7 +559,7 @@ pub trait ThemeDraw { rect: Rect, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ); /// Method used to implement [`DrawCx::text_with_selection`] diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 55ff1a7ed..9cbcb834e 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -355,7 +355,7 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { rect: Rect, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ) { let bb = Quad::conv(rect); let col; @@ -399,21 +399,18 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { }); let effects = [ - Effect { - start: 0, + (0, Effect { color: 0, flags: Default::default(), - }, - Effect { - start: range.start.cast(), + }), + (range.start.cast(), Effect { color: 1, flags: Default::default(), - }, - Effect { - start: range.end.cast(), + }), + (range.end.cast(), Effect { color: 0, flags: Default::default(), - }, + }), ]; let colors = [col, sel_col]; self.draw.text_effects(pos, bb, text, &colors, &effects); diff --git a/crates/kas-core/src/theme/text.rs b/crates/kas-core/src/theme/text.rs index 59c470b8b..ac4d1eb6c 100644 --- a/crates/kas-core/src/theme/text.rs +++ b/crates/kas-core/src/theme/text.rs @@ -395,7 +395,7 @@ impl Text { /// preferable not to use this method (use a dummy implementation returning /// `&[]` and use inherent methods on the text object via [`Text::text`]). #[inline] - pub fn effect_tokens(&self) -> &[Effect] { + pub fn effect_tokens(&self) -> &[(u32, Effect)] { self.text.effect_tokens() } } diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index 425b906a0..7513b1a2d 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -87,7 +87,7 @@ impl Methods { rect: Rect, text: &TextDisplay, colors: &[Rgba], - effects: &[::kas::text::Effect], + effects: &[(u32, ::kas::text::Effect)], ) { (#base).text_effects(id, pos, rect, text, colors, effects); } diff --git a/crates/kas-soft/src/draw.rs b/crates/kas-soft/src/draw.rs index 416edd9af..2494ef6e3 100644 --- a/crates/kas-soft/src/draw.rs +++ b/crates/kas-soft/src/draw.rs @@ -186,7 +186,7 @@ impl DrawSharedImpl for Shared { bb: Quad, text: &text::TextDisplay, colors: &[color::Rgba], - effects: &[text::Effect], + effects: &[(u32, text::Effect)], ) { let time = std::time::Instant::now(); self.text.text_effects( diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 0b7b81228..57ce6f004 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -364,7 +364,7 @@ impl DrawSharedImpl for DrawPipe { bb: Quad, text: &TextDisplay, colors: &[Rgba], - effects: &[Effect], + effects: &[(u32, Effect)], ) { let time = std::time::Instant::now(); self.text.text_effects( diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 1d5089bf8..127b7e9ca 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -166,21 +166,18 @@ impl Component { if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { // TODO: combine underline with selection highlight let effects = [ - Effect { - start: 0, + (0, Effect { color: 0, flags: Default::default(), - }, - Effect { - start: edit_range.start, + }), + (edit_range.start, Effect { color: 0, flags: EffectFlags::UNDERLINE, - }, - Effect { - start: edit_range.end, + }), + (edit_range.end, Effect { color: 0, flags: Default::default(), - }, + }), ]; if let Ok(display) = self.text.display() { draw.text_with_effects(pos, rect, display, &[], &effects); From ecdb3b662e2e6d78f6c086ef0481d0c7cb487abd Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 18 Feb 2026 09:48:59 +0000 Subject: [PATCH 02/14] Fix: ensure effects list starts are strictly increasing --- crates/kas-core/src/theme/simple_theme.rs | 14 +++++--------- crates/kas-widgets/src/edit/editor.rs | 13 ++++--------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 9cbcb834e..8af600c74 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -399,21 +399,17 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { }); let effects = [ - (0, Effect { - color: 0, - flags: Default::default(), - }), + Default::default(), (range.start.cast(), Effect { color: 1, flags: Default::default(), }), - (range.end.cast(), Effect { - color: 0, - flags: Default::default(), - }), + (range.end.cast(), Effect::default()), ]; + let r0 = if range.start > 0 { 0 } else { 1 }; let colors = [col, sel_col]; - self.draw.text_effects(pos, bb, text, &colors, &effects); + self.draw + .text_effects(pos, bb, text, &colors, &effects[r0..]); } fn text_cursor(&mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay, byte: usize) { diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 127b7e9ca..0f93d4dd2 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -166,21 +166,16 @@ impl Component { if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { // TODO: combine underline with selection highlight let effects = [ - (0, Effect { - color: 0, - flags: Default::default(), - }), + Default::default(), (edit_range.start, Effect { color: 0, flags: EffectFlags::UNDERLINE, }), - (edit_range.end, Effect { - color: 0, - flags: Default::default(), - }), + (edit_range.end, Effect::default()), ]; + let r0 = if edit_range.start > 0 { 0 } else { 1 }; if let Ok(display) = self.text.display() { - draw.text_with_effects(pos, rect, display, &[], &effects); + draw.text_with_effects(pos, rect, display, &[], &effects[r0..]); } } else { draw.text_with_selection(pos, rect, &self.text, self.selection.range()); From cd34695a384cca7b07cf3e262ceddea37cfebcf7 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 17 Feb 2026 10:30:01 +0000 Subject: [PATCH 03/14] Update for changes to GlyphRun::glyphs_with_effects --- Cargo.toml | 2 +- crates/kas-core/src/text/raster.rs | 47 +++++++++++++++++++++++------- examples/gallery.rs | 2 +- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 63382eb57..61c4d577c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,4 +172,4 @@ resolver = "2" [patch.crates-io.kas-text] git = "https://github.com/kas-gui/kas-text.git" -rev = "62a35caa" +rev = "8f6434e0" diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index fc30a7704..9775af775 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -15,7 +15,7 @@ use kas::config::RasterConfig; use kas::draw::{AllocError, Allocation, PassId, color::Rgba}; use kas::geom::{Quad, Vec2}; use kas_text::fonts::{self, FaceId}; -use kas_text::{Effect, Glyph, GlyphId, TextDisplay}; +use kas_text::{Effect, EffectFlags, Glyph, GlyphId, TextDisplay}; use rustc_hash::FxHashMap as HashMap; use swash::zeno::Format; @@ -561,7 +561,7 @@ impl State { for run in text.runs(pos.into(), effects) { let face = run.face_id(); let dpem = run.dpem(); - let for_glyph = |glyph: Glyph, e: u16| { + let for_glyph = |glyph: Glyph, effect: Effect| { let desc = SpriteDescriptor::new(&self.config, face, glyph, dpem); let sprite = match self.glyphs.get(&desc) { Some(sprite) => sprite, @@ -573,20 +573,47 @@ impl State { } } }; - let col = colors.get(usize::conv(e)).cloned().unwrap_or(Rgba::BLACK); + let col = colors + .get(usize::conv(effect.color)) + .cloned() + .unwrap_or(Rgba::BLACK); queue.push_sprite(pass, glyph.position.into(), bb, col, sprite); }; - let for_rect = |x1, x2, y: f32, h: f32, e: u16| { - let y = y.ceil(); - let y2 = y + h.ceil(); - if let Some(quad) = Quad::from_coords(Vec2(x1, y), Vec2(x2, y2)).intersection(&bb) { - let col = colors.get(usize::conv(e)).cloned().unwrap_or(Rgba::BLACK); - draw_quad(quad, col); + let sf = run.scaled_face(); + let for_range = |p: kas_text::Vec2, x2, effect: Effect| { + if effect.flags.is_empty() { + return; + } + + let p: Vec2 = p.into(); + let col = colors + .get(usize::conv(effect.color)) + .cloned() + .unwrap_or(Rgba::BLACK); + + if effect.flags.contains(EffectFlags::UNDERLINE) + && let Some(metrics) = sf.underline_metrics() + { + let a = Vec2(p.0, p.1 - metrics.top.ceil()); + let b = Vec2(x2, a.1 + metrics.thickness.ceil()); + if let Some(quad) = Quad::from_coords(a, b).intersection(&bb) { + draw_quad(quad, col); + } + } + + if effect.flags.contains(EffectFlags::STRIKETHROUGH) + && let Some(metrics) = sf.strikethrough_metrics() + { + let a = Vec2(p.0, p.1 - metrics.top.ceil()); + let b = Vec2(x2, a.1 + metrics.thickness.ceil()); + if let Some(quad) = Quad::from_coords(a, b).intersection(&bb) { + draw_quad(quad, col); + } } }; - run.glyphs_with_effects(for_glyph, for_rect); + run.glyphs_with_effects(for_glyph, for_range); } } } diff --git a/examples/gallery.rs b/examples/gallery.rs index bb154ef59..e5f77aefe 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -305,7 +305,7 @@ fn editor() -> Page { Demonstration of *as-you-type* formatting from **Markdown**. 1. Edit below -2. View the result +2. View the ~~result~~ _result_ 3. In case of error, be informed ### Not all Markdown supported From 45f5d91c55c6a9c82f558886419afacc30353c56 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 17 Feb 2026 14:53:47 +0000 Subject: [PATCH 04/14] Update: expect iter font_tokens to be non-empty --- Cargo.toml | 2 +- crates/kas-core/src/text/string.rs | 4 ++-- crates/kas-core/src/theme/text.rs | 11 +++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 61c4d577c..f51dc5146 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,4 +172,4 @@ resolver = "2" [patch.crates-io.kas-text] git = "https://github.com/kas-gui/kas-text.git" -rev = "8f6434e0" +rev = "742f576e" diff --git a/crates/kas-core/src/text/string.rs b/crates/kas-core/src/text/string.rs index 0a5f31f76..7816f351d 100644 --- a/crates/kas-core/src/text/string.rs +++ b/crates/kas-core/src/text/string.rs @@ -105,8 +105,8 @@ impl FormattableText for AccessString { } #[inline] - fn font_tokens(&self, _: f32, _: FontSelector) -> impl Iterator { - std::iter::empty() + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + std::iter::once(FontToken { start: 0, dpem, font }) } fn effect_tokens(&self) -> &[(u32, Effect)] { diff --git a/crates/kas-core/src/theme/text.rs b/crates/kas-core/src/theme/text.rs index ac4d1eb6c..3e13ed719 100644 --- a/crates/kas-core/src/theme/text.rs +++ b/crates/kas-core/src/theme/text.rs @@ -448,9 +448,16 @@ impl Text { match self.status { Status::New => self .display - .prepare_runs(&self.text, self.direction, self.font, self.dpem) + .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, self.font, self.dpem), + Status::ResizeLevelRuns => self.display.resize_runs( + self.text.as_str(), + self.text.font_tokens(self.dpem, self.font), + ), _ => return, } From 4d209899b8f7edb6a8451c0d0a6ca855b40f040c Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 17 Feb 2026 10:31:35 +0000 Subject: [PATCH 05/14] Add local copy of trait FormattableText --- crates/kas-core/src/text/format.rs | 65 +++++++++++++++++++++++++++ crates/kas-core/src/text/mod.rs | 4 +- crates/kas-core/src/text/selection.rs | 2 +- crates/kas-core/src/text/string.rs | 6 ++- 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 crates/kas-core/src/text/format.rs diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs new file mode 100644 index 000000000..9968c352c --- /dev/null +++ b/crates/kas-core/src/text/format.rs @@ -0,0 +1,65 @@ +// 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 + +//! Formatted text traits and types + +use super::Effect; +use super::fonts::FontSelector; +pub use kas_text::format::*; + +/// Text, optionally with formatting data +pub trait FormattableText: std::cmp::PartialEq { + /// Access whole text as contiguous `str` + fn as_str(&self) -> &str; + + /// Return an iterator of font tokens + /// + /// 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 + /// values are passed as a reference (`dpem` and `font`). + /// + /// The iterator is expected to yield a stream of tokens such that + /// [`FontToken::start`] values are strictly increasing, less than + /// `self.as_str().len()` and at `char` boundaries (i.e. an index value + /// returned by [`str::char_indices`]. In case the returned iterator is + /// empty or the first [`FontToken::start`] value is greater than zero the + /// reference `dpem` and `font` values are used. + /// + /// Any changes to the result of this method require full re-preparation of + /// text since this affects run breaking and font resolution. + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator; + + /// Return the sequence of effect tokens + /// + /// The `effects` sequence may be used for rendering effects: glyph color, + /// background color, strike-through, underline. Use `&[]` for no effects + /// (effectively using the default value of `Self::Effect` everywhere), 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 `effects[i].1` + /// where `i` is the largest value such that `effects[i].0 <= j`, or the + /// default value of `Self::Effect` if no such `i` exists. + /// + /// Changes to the result of this method do not require any re-preparation + /// of text. + fn effect_tokens(&self) -> &[(u32, Effect)]; +} + +impl FormattableText for T { + #[inline] + fn as_str(&self) -> &str { + self.as_str() + } + + #[inline] + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + self.font_tokens(dpem, font) + } + + #[inline] + fn effect_tokens(&self) -> &[(u32, Effect)] { + self.effect_tokens() + } +} diff --git a/crates/kas-core/src/text/mod.rs b/crates/kas-core/src/text/mod.rs index 9e86c9a17..dc45de5bb 100644 --- a/crates/kas-core/src/text/mod.rs +++ b/crates/kas-core/src/text/mod.rs @@ -15,9 +15,11 @@ pub use kas_text::{ Align, DPU, Direction, Effect, EffectFlags, Line, LineIterator, MarkerPos, MarkerPosIter, - NotReady, Status, TextDisplay, Vec2, fonts, format, + NotReady, Status, TextDisplay, Vec2, fonts, }; +pub mod format; + /// Glyph rastering #[cfg_attr(not(feature = "internal_doc"), doc(hidden))] #[cfg_attr(docsrs, doc(cfg(internal_doc)))] diff --git a/crates/kas-core/src/text/selection.rs b/crates/kas-core/src/text/selection.rs index 5d03c2b26..7570e687c 100644 --- a/crates/kas-core/src/text/selection.rs +++ b/crates/kas-core/src/text/selection.rs @@ -5,9 +5,9 @@ //! Tools for text selection +use crate::text::format::FormattableText; use crate::theme::Text; use kas_macros::autoimpl; -use kas_text::format::FormattableText; use std::ops::Range; use unicode_segmentation::UnicodeSegmentation; diff --git a/crates/kas-core/src/text/string.rs b/crates/kas-core/src/text/string.rs index 7816f351d..934d674f5 100644 --- a/crates/kas-core/src/text/string.rs +++ b/crates/kas-core/src/text/string.rs @@ -106,7 +106,11 @@ impl FormattableText for AccessString { #[inline] fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { - std::iter::once(FontToken { start: 0, dpem, font }) + std::iter::once(FontToken { + start: 0, + dpem, + font, + }) } fn effect_tokens(&self) -> &[(u32, Effect)] { From ac51c32a1c1f808d5f05bc486ff3f0a170f306c2 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 17 Feb 2026 17:14:45 +0000 Subject: [PATCH 06/14] Update: move Effect to kas-core --- Cargo.toml | 2 +- crates/kas-core/src/draw/draw.rs | 2 +- crates/kas-core/src/draw/draw_shared.rs | 2 +- crates/kas-core/src/text/format.rs | 69 +++++++++++++++++++++-- crates/kas-core/src/text/mod.rs | 4 +- crates/kas-core/src/text/raster.rs | 5 +- crates/kas-core/src/text/string.rs | 4 +- crates/kas-core/src/theme/draw.rs | 2 +- crates/kas-core/src/theme/simple_theme.rs | 2 +- crates/kas-core/src/theme/text.rs | 2 +- crates/kas-macros/src/extends.rs | 2 +- crates/kas-soft/src/draw.rs | 2 +- crates/kas-wgpu/src/draw/draw_pipe.rs | 2 +- crates/kas-widgets/src/edit/editor.rs | 3 +- 14 files changed, 81 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f51dc5146..8ddcabeee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,4 +172,4 @@ resolver = "2" [patch.crates-io.kas-text] git = "https://github.com/kas-gui/kas-text.git" -rev = "742f576e" +rev = "dc34ef6212dbe8ba539d50a0a3bff1bf2cbad102" diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index 6ddeefdd9..d2e169995 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -9,7 +9,7 @@ use super::{AnimationState, color::Rgba}; #[allow(unused)] use super::{DrawRounded, DrawRoundedImpl}; use super::{DrawShared, DrawSharedImpl, ImageId, PassId, PassType, SharedState, WindowCommon}; use crate::geom::{Offset, Quad, Rect, Vec2}; -use crate::text::{Effect, TextDisplay}; +use crate::text::{TextDisplay, format::Effect}; use std::any::Any; use std::time::Instant; diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index 554c631c2..9957f3da0 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -10,7 +10,7 @@ use super::{DrawImpl, PassId}; use crate::ActionRedraw; use crate::config::RasterConfig; use crate::geom::{Quad, Size, Vec2}; -use crate::text::{Effect, TextDisplay}; +use crate::text::{TextDisplay, format::Effect}; use std::any::Any; use std::num::NonZeroU32; use std::sync::Arc; diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index 9968c352c..aa40260b4 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -5,9 +5,33 @@ //! Formatted text traits and types -use super::Effect; use super::fonts::FontSelector; -pub use kas_text::format::*; +pub use kas_text::format::FontToken; + +/// Effect formatting marker +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Effect { + /// User-specified value + /// + /// Usage is not specified by `kas-text`, but typically this field will be + /// used as an index into a colour palette or not used at all. + pub color: u16, + /// Effect flags + pub flags: EffectFlags, +} + +bitflags::bitflags! { + /// Text effects + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct EffectFlags: u16 { + /// Glyph is underlined + const UNDERLINE = 1 << 0; + /// Glyph is crossed through by a center-line + const STRIKETHROUGH = 1 << 1; + } +} /// Text, optionally with formatting data pub trait FormattableText: std::cmp::PartialEq { @@ -47,19 +71,52 @@ pub trait FormattableText: std::cmp::PartialEq { fn effect_tokens(&self) -> &[(u32, Effect)]; } -impl FormattableText for T { +impl FormattableText for str { #[inline] fn as_str(&self) -> &str { - self.as_str() + self } #[inline] fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { - self.font_tokens(dpem, font) + let start = 0; + std::iter::once(FontToken { start, dpem, font }) + } + + #[inline] + fn effect_tokens(&self) -> &[(u32, Effect)] { + &[] + } +} + +impl FormattableText for String { + #[inline] + fn as_str(&self) -> &str { + self } #[inline] + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + let start = 0; + std::iter::once(FontToken { start, dpem, font }) + } + + #[inline] + fn effect_tokens(&self) -> &[(u32, Effect)] { + &[] + } +} + +impl FormattableText for &F { + fn as_str(&self) -> &str { + F::as_str(self) + } + + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + F::font_tokens(self, dpem, font) + } + fn effect_tokens(&self) -> &[(u32, Effect)] { - self.effect_tokens() + F::effect_tokens(self) } } diff --git a/crates/kas-core/src/text/mod.rs b/crates/kas-core/src/text/mod.rs index dc45de5bb..e8600687f 100644 --- a/crates/kas-core/src/text/mod.rs +++ b/crates/kas-core/src/text/mod.rs @@ -14,8 +14,8 @@ //! [KAS Text]: https://github.com/kas-gui/kas-text/ pub use kas_text::{ - Align, DPU, Direction, Effect, EffectFlags, Line, LineIterator, MarkerPos, MarkerPosIter, - NotReady, Status, TextDisplay, Vec2, fonts, + Align, DPU, Direction, Line, LineIterator, MarkerPos, MarkerPosIter, NotReady, Status, + TextDisplay, Vec2, fonts, }; pub mod format; diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index 9775af775..f7143ba4c 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -14,8 +14,9 @@ use kas::cast::traits::*; use kas::config::RasterConfig; use kas::draw::{AllocError, Allocation, PassId, color::Rgba}; use kas::geom::{Quad, Vec2}; +use kas::text::format::{Effect, EffectFlags}; use kas_text::fonts::{self, FaceId}; -use kas_text::{Effect, EffectFlags, Glyph, GlyphId, TextDisplay}; +use kas_text::{Glyph, GlyphId, TextDisplay}; use rustc_hash::FxHashMap as HashMap; use swash::zeno::Format; @@ -512,7 +513,7 @@ impl State { text: &TextDisplay, col: Rgba, ) { - for run in text.runs(pos.into(), &[]) { + for run in text.runs::<()>(pos.into(), &[]) { let face = run.face_id(); let dpem = run.dpem(); for glyph in run.glyphs() { diff --git a/crates/kas-core/src/text/string.rs b/crates/kas-core/src/text/string.rs index 934d674f5..afed88ee9 100644 --- a/crates/kas-core/src/text/string.rs +++ b/crates/kas-core/src/text/string.rs @@ -11,8 +11,8 @@ use crate::cast::Conv; use crate::event::Key; -use crate::text::format::{FontToken, FormattableText}; -use crate::text::{Effect, EffectFlags, fonts::FontSelector}; +use crate::text::fonts::FontSelector; +use crate::text::format::{Effect, EffectFlags, FontToken, FormattableText}; /// An access key string /// diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 7d279ffce..9f6e9f44b 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -14,7 +14,7 @@ use crate::draw::{Draw, DrawIface, DrawRounded, DrawShared, DrawSharedImpl, Imag use crate::event::EventState; #[allow(unused)] use crate::event::{Command, ConfigCx}; use crate::geom::{Coord, Offset, Rect}; -use crate::text::{Effect, TextDisplay, format::FormattableText}; +use crate::text::{TextDisplay, format::Effect, format::FormattableText}; use crate::theme::ColorsLinear; use crate::{Id, Tile, autoimpl}; #[allow(unused)] use crate::{Layout, theme::TextClass}; diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 8af600c74..9221c232d 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -17,7 +17,7 @@ use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; use crate::geom::*; -use crate::text::{Effect, TextDisplay}; +use crate::text::{TextDisplay, format::Effect}; use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle}; use crate::theme::{ColorsLinear, InputState, Theme}; diff --git a/crates/kas-core/src/theme/text.rs b/crates/kas-core/src/theme/text.rs index 3e13ed719..3eef31bfe 100644 --- a/crates/kas-core/src/theme/text.rs +++ b/crates/kas-core/src/theme/text.rs @@ -15,7 +15,7 @@ use crate::cast::Cast; use crate::geom::{Rect, Vec2}; use crate::layout::{AlignHints, AxisInfo, SizeRules, Stretch}; use crate::text::fonts::FontSelector; -use crate::text::format::FormattableText; +use crate::text::format::{Effect, FormattableText}; use crate::text::*; use std::num::NonZeroUsize; diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index 7513b1a2d..ea68a2df4 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -87,7 +87,7 @@ impl Methods { rect: Rect, text: &TextDisplay, colors: &[Rgba], - effects: &[(u32, ::kas::text::Effect)], + effects: &[(u32, ::kas::text::format::Effect)], ) { (#base).text_effects(id, pos, rect, text, colors, effects); } diff --git a/crates/kas-soft/src/draw.rs b/crates/kas-soft/src/draw.rs index 2494ef6e3..7b5182ece 100644 --- a/crates/kas-soft/src/draw.rs +++ b/crates/kas-soft/src/draw.rs @@ -186,7 +186,7 @@ impl DrawSharedImpl for Shared { bb: Quad, text: &text::TextDisplay, colors: &[color::Rgba], - effects: &[(u32, text::Effect)], + effects: &[(u32, text::format::Effect)], ) { let time = std::time::Instant::now(); self.text.text_effects( diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 57ce6f004..465fe4c49 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -18,7 +18,7 @@ use kas::draw::color::Rgba; use kas::draw::*; use kas::geom::{Quad, Size, Vec2}; use kas::runner::{GraphicsFeatures, RunError}; -use kas::text::{Effect, TextDisplay}; +use kas::text::{TextDisplay, format::Effect}; impl DrawPipe { /// Construct diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 0f93d4dd2..e18c9c7ec 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -10,7 +10,8 @@ use kas::event::components::{TextInput, TextInputAction}; use kas::event::{ElementState, FocusSource, Ime, ImePurpose, ImeSurroundingText, Scroll}; use kas::geom::Vec2; use kas::prelude::*; -use kas::text::{CursorRange, Effect, EffectFlags, NotReady, SelectionHelper}; +use kas::text::format::{Effect, EffectFlags}; +use kas::text::{CursorRange, NotReady, SelectionHelper}; use kas::theme::{Text, TextClass}; use kas::util::UndoStack; use std::borrow::Cow; From 8feebe549a5dd1d0ea0fc1af80019abf762b8f48 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 18 Feb 2026 08:35:37 +0000 Subject: [PATCH 07/14] Split text effects into color-effects and decorations --- crates/kas-core/src/draw/draw.rs | 46 ++++++--- crates/kas-core/src/draw/draw_shared.rs | 32 ++++-- crates/kas-core/src/text/format.rs | 120 +++++++++++++++------- crates/kas-core/src/text/raster.rs | 100 ++++++++++-------- crates/kas-core/src/text/string.rs | 21 ++-- crates/kas-core/src/theme/draw.rs | 117 +++++++++++++++------ crates/kas-core/src/theme/simple_theme.rs | 50 ++++++--- crates/kas-core/src/theme/text.rs | 23 +++-- crates/kas-macros/src/extends.rs | 18 +++- crates/kas-soft/src/draw.rs | 28 ++++- crates/kas-wgpu/src/draw/draw_pipe.rs | 30 ++++-- crates/kas-widgets/src/access_label.rs | 7 +- crates/kas-widgets/src/edit/editor.rs | 20 ++-- 13 files changed, 418 insertions(+), 194 deletions(-) diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index d2e169995..f334ec00d 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -9,7 +9,7 @@ use super::{AnimationState, color::Rgba}; #[allow(unused)] use super::{DrawRounded, DrawRoundedImpl}; use super::{DrawShared, DrawSharedImpl, ImageId, PassId, PassType, SharedState, WindowCommon}; use crate::geom::{Offset, Quad, Rect, Vec2}; -use crate::text::{TextDisplay, format::Effect}; +use crate::text::{TextDisplay, format}; use std::any::Any; use std::time::Instant; @@ -223,16 +223,10 @@ pub trait Draw { /// Draw the image in the given `rect` fn image(&mut self, id: ImageId, rect: Quad); - /// Draw text with effects + /// Draw text with a list of color effects /// /// Text is drawn from `pos` and clipped to `bounding_box`. /// - /// The `effects` list provides underlining/strikethrough information via - /// [`Effect::flags`] and an index [`Effect::color`]. - /// - /// Text colour lookup uses index `color` and is essentially: - /// `colors.get(color.unwrap_or(Rgba::BLACK)`. - /// /// The `text` display must be prepared prior to calling this method. /// Typically this is done using a [`crate::theme::Text`] object. fn text_effects( @@ -240,8 +234,21 @@ pub trait Draw { pos: Vec2, bounding_box: Quad, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, format::Colors)], + ); + + /// 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. + fn decorate_text( + &mut self, + pos: Vec2, + bounding_box: Quad, + text: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, format::Decoration)], ); } @@ -298,12 +305,25 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { pos: Vec2, bb: Quad, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, format::Colors)], + ) { + self.shared + .draw + .draw_text_effects(self.draw, self.pass, pos, bb, text, palette, tokens); + } + + fn decorate_text( + &mut self, + pos: Vec2, + bb: Quad, + text: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, format::Decoration)], ) { self.shared .draw - .draw_text_effects(self.draw, self.pass, pos, bb, text, colors, effects); + .decorate_text(self.draw, self.pass, pos, bb, text, palette, decorations); } } diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index 9957f3da0..9d3e9cf6a 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -10,7 +10,7 @@ use super::{DrawImpl, PassId}; use crate::ActionRedraw; use crate::config::RasterConfig; use crate::geom::{Quad, Size, Vec2}; -use crate::text::{TextDisplay, format::Effect}; +use crate::text::{TextDisplay, format}; use std::any::Any; use std::num::NonZeroU32; use std::sync::Arc; @@ -201,21 +201,35 @@ pub trait DrawSharedImpl: Any { /// Draw the image in the given `rect` fn draw_image(&self, draw: &mut Self::Draw, pass: PassId, id: ImageId, rect: Quad); - /// Draw text with effects + /// Draw text with a list of color effects /// - /// The `effects` list provides underlining/strikethrough information via - /// [`Effect::flags`] and an index [`Effect::color`]. + /// Text is drawn from `pos` and clipped to `bounding_box`. /// - /// Text colour lookup uses index `color` and is essentially: - /// `colors.get(color.unwrap_or(Rgba::BLACK)`. + /// The `text` display must be prepared prior to calling this method. + /// Typically this is done using a [`crate::theme::Text`] object. fn draw_text_effects( &mut self, draw: &mut Self::Draw, pass: PassId, pos: Vec2, - bb: Quad, + bounding_box: Quad, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, format::Colors)], + ); + + /// 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. + fn decorate_text( + &mut self, + draw: &mut Self::Draw, + pass: PassId, + pos: Vec2, + bounding_box: Quad, + text: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, format::Decoration)], ); } diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index aa40260b4..55b7b09b4 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -8,29 +8,49 @@ use super::fonts::FontSelector; pub use kas_text::format::FontToken; -/// Effect formatting marker +/// Effect formatting marker: text and background color #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Effect { +pub struct Colors { /// User-specified value /// /// Usage is not specified by `kas-text`, but typically this field will be /// used as an index into a colour palette or not used at all. pub color: u16, - /// Effect flags - pub flags: EffectFlags, } -bitflags::bitflags! { - /// Text effects - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct EffectFlags: u16 { - /// Glyph is underlined - const UNDERLINE = 1 << 0; - /// Glyph is crossed through by a center-line - const STRIKETHROUGH = 1 << 1; - } +/// Decoration types +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum DecorationType { + /// No decoration + #[default] + None, + /// Glyph is underlined + Underline, + /// Glyph is crossed through by a center-line + Strikethrough, +} + +/// Decoration styles +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum LineStyle { + /// A single solid line + #[default] + Solid, +} + +/// Effect formatting marker: strikethrough and underline decorations +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Decoration { + /// Type of decoration + pub dec: DecorationType, + /// Line style + pub style: LineStyle, + /// Line color + pub color: u16, } /// Text, optionally with formatting data @@ -56,19 +76,44 @@ pub trait FormattableText: std::cmp::PartialEq { /// text since this affects run breaking and font resolution. fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator; - /// Return the sequence of effect tokens + /// Return the sequence of color effect tokens + /// + /// These tokens may be used for rendering effects: glyph color and + /// background color. + /// + /// Use `&[]` to use the default `Colors` everywhere, or use a sequence such + /// that `tokens[i].0` values are strictly increasing. A glyph for index `j` + /// in the source text will use the colors `tokens[i].1` where `i` is the + /// largest value such that `tokens[i].0 <= j`, or the default `Colors` if + /// no such `i` exists. + /// + /// Changes to the result of this method do not require any re-preparation + /// of text. + /// + /// The default implementation returns `&[]`. + #[inline] + fn color_tokens(&self) -> &[(u32, Colors)] { + &[] + } + + /// Return optional sequences of decoration tokens + /// + /// These tokens may be used for rendering effects: strike-through and + /// underline decorations. /// - /// The `effects` sequence may be used for rendering effects: glyph color, - /// background color, strike-through, underline. Use `&[]` for no effects - /// (effectively using the default value of `Self::Effect` everywhere), 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 `effects[i].1` - /// where `i` is the largest value such that `effects[i].0 <= j`, or the - /// default value of `Self::Effect` if no such `i` exists. + /// Use `&[]` for no decorations, or use a sequence such that `tokens[i].0` + /// values are strictly increasing. A glyph for index `j` in the source text + /// will use the decoration `tokens[i].1` where `i` is the largest value + /// such that `tokens[i].0 <= j`, or no decoration if no such `i` exists. /// /// Changes to the result of this method do not require any re-preparation /// of text. - fn effect_tokens(&self) -> &[(u32, Effect)]; + /// + /// The default implementation returns `&[]`. + #[inline] + fn decorations(&self) -> &[(u32, Decoration)] { + &[] + } } impl FormattableText for str { @@ -82,11 +127,6 @@ impl FormattableText for str { let start = 0; std::iter::once(FontToken { start, dpem, font }) } - - #[inline] - fn effect_tokens(&self) -> &[(u32, Effect)] { - &[] - } } impl FormattableText for String { @@ -100,11 +140,6 @@ impl FormattableText for String { let start = 0; std::iter::once(FontToken { start, dpem, font }) } - - #[inline] - fn effect_tokens(&self) -> &[(u32, Effect)] { - &[] - } } impl FormattableText for &F { @@ -116,7 +151,22 @@ impl FormattableText for &F { F::font_tokens(self, dpem, font) } - fn effect_tokens(&self) -> &[(u32, Effect)] { - F::effect_tokens(self) + fn color_tokens(&self) -> &[(u32, Colors)] { + F::color_tokens(self) } + + fn decorations(&self) -> &[(u32, Decoration)] { + F::decorations(self) + } +} + +#[cfg(test)] +#[test] +fn sizes() { + use std::mem::size_of; + + assert_eq!(size_of::(), 2); + assert_eq!(size_of::(), 1); + assert_eq!(size_of::(), 0); + assert_eq!(size_of::(), 4); } diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index f7143ba4c..53d860228 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -10,18 +10,17 @@ //! Text drawing pipeline +use crate::config::SubpixelMode; +use crate::text::format::{Colors, Decoration, DecorationType, LineStyle}; use kas::cast::traits::*; use kas::config::RasterConfig; use kas::draw::{AllocError, Allocation, PassId, color::Rgba}; use kas::geom::{Quad, Vec2}; -use kas::text::format::{Effect, EffectFlags}; use kas_text::fonts::{self, FaceId}; use kas_text::{Glyph, GlyphId, TextDisplay}; use rustc_hash::FxHashMap as HashMap; use swash::zeno::Format; -use crate::config::SubpixelMode; - /// Number of sub-pixel text sizes /// /// Text `dpem * SCALE_STEPS` is rounded to the nearest integer, so for example @@ -502,7 +501,7 @@ impl State { } } - /// Draw text as a sequence of sprites + /// Draw text without effects pub fn text( &mut self, allocator: &mut dyn SpriteAllocator, @@ -533,7 +532,7 @@ impl State { } } - /// Draw text with effects as a sequence of sprites + /// Draw text with color effects #[allow(clippy::too_many_arguments)] pub fn text_effects( &mut self, @@ -543,26 +542,26 @@ impl State { pos: Vec2, bb: Quad, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, Colors)], mut draw_quad: impl FnMut(Quad, Rgba), ) { // Optimisation: use cheaper TextDisplay::runs method - if effects.len() <= 1 - && effects + if tokens.len() <= 1 + && tokens .first() - .map(|e| e.1.flags == Default::default()) + .map(|e| e.1 == Default::default()) .unwrap_or(true) { - let col = colors.first().cloned().unwrap_or(Rgba::BLACK); + let col = palette.first().cloned().unwrap_or(Rgba::BLACK); self.text(allocator, queue, pass, pos, bb, text, col); return; } - for run in text.runs(pos.into(), effects) { + for run in text.runs(pos.into(), tokens) { let face = run.face_id(); let dpem = run.dpem(); - let for_glyph = |glyph: Glyph, effect: Effect| { + let for_glyph = |glyph: Glyph, token: Colors| { let desc = SpriteDescriptor::new(&self.config, face, glyph, dpem); let sprite = match self.glyphs.get(&desc) { Some(sprite) => sprite, @@ -574,47 +573,68 @@ impl State { } } }; - let col = colors - .get(usize::conv(effect.color)) + let col = palette + .get(usize::conv(token.color)) .cloned() .unwrap_or(Rgba::BLACK); queue.push_sprite(pass, glyph.position.into(), bb, col, sprite); }; let sf = run.scaled_face(); - let for_range = |p: kas_text::Vec2, x2, effect: Effect| { - if effect.flags.is_empty() { + let for_range = |p: kas_text::Vec2, x2, token: Colors| {}; + + run.glyphs_with_effects(for_glyph, for_range); + } + } + + /// Draw text decorations (e.g. underlines) + #[allow(clippy::too_many_arguments)] + pub fn decorate_text( + &mut self, + pos: Vec2, + bb: Quad, + text: &TextDisplay, + palette: &[Rgba], + tokens: &[(u32, Decoration)], + mut draw_quad: impl FnMut(Quad, Rgba), + ) { + // Optimisation: do nothing + if tokens.len() <= 1 + && tokens + .first() + .map(|e| e.1 == Default::default()) + .unwrap_or(true) + { + return; + } + + for run in text.runs(pos.into(), tokens) { + let sf = run.scaled_face(); + let for_range = |p: kas_text::Vec2, x2, token: Decoration| { + let metrics = match token.dec { + DecorationType::None => return, + DecorationType::Underline => sf.underline_metrics(), + DecorationType::Strikethrough => sf.strikethrough_metrics(), + }; + let Some(metrics) = metrics else { return }; + + let a = Vec2(p.0, p.1 - metrics.top.ceil()); + let b = Vec2(x2, a.1 + metrics.thickness.ceil()); + let Some(quad) = Quad::from_coords(a, b).intersection(&bb) else { return; - } + }; - let p: Vec2 = p.into(); - let col = colors - .get(usize::conv(effect.color)) + let col = palette + .get(usize::conv(token.color)) .cloned() .unwrap_or(Rgba::BLACK); - if effect.flags.contains(EffectFlags::UNDERLINE) - && let Some(metrics) = sf.underline_metrics() - { - let a = Vec2(p.0, p.1 - metrics.top.ceil()); - let b = Vec2(x2, a.1 + metrics.thickness.ceil()); - if let Some(quad) = Quad::from_coords(a, b).intersection(&bb) { - draw_quad(quad, col); - } - } - - if effect.flags.contains(EffectFlags::STRIKETHROUGH) - && let Some(metrics) = sf.strikethrough_metrics() - { - let a = Vec2(p.0, p.1 - metrics.top.ceil()); - let b = Vec2(x2, a.1 + metrics.thickness.ceil()); - if let Some(quad) = Quad::from_coords(a, b).intersection(&bb) { - draw_quad(quad, col); - } + match token.style { + LineStyle::Solid => draw_quad(quad, col), } }; - run.glyphs_with_effects(for_glyph, for_range); + run.glyphs_with_effects(|_, _| (), for_range); } } } diff --git a/crates/kas-core/src/text/string.rs b/crates/kas-core/src/text/string.rs index afed88ee9..a7bc85645 100644 --- a/crates/kas-core/src/text/string.rs +++ b/crates/kas-core/src/text/string.rs @@ -12,7 +12,7 @@ use crate::cast::Conv; use crate::event::Key; use crate::text::fonts::FontSelector; -use crate::text::format::{Effect, EffectFlags, FontToken, FormattableText}; +use crate::text::format::{Decoration, DecorationType, FontToken, FormattableText}; /// An access key string /// @@ -29,7 +29,7 @@ use crate::text::format::{Effect, EffectFlags, FontToken, FormattableText}; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct AccessString { text: String, - key: Option<(Key, [(u32, Effect); 2])>, + key: Option<(Key, [(u32, Decoration); 2])>, } impl AccessString { @@ -60,18 +60,15 @@ impl AccessString { let k = c.to_ascii_lowercase().encode_utf8(&mut kbuf); let k = Key::Character(k.into()); - let e0 = (start, Effect { - color: 0, - flags: EffectFlags::UNDERLINE, + let e0 = (start, Decoration { + dec: DecorationType::Underline, + ..Decoration::default() }); let i = c.len_utf8(); s = &s[i..]; - let e1 = (start + u32::conv(i), Effect { - color: 0, - flags: EffectFlags::empty(), - }); + let e1 = (start + u32::conv(i), Decoration::default()); key = Some((k, [e0, e1])); } @@ -88,7 +85,7 @@ impl AccessString { } /// Get the key bindings and associated effects, if any - pub fn key(&self) -> Option<&(Key, [(u32, Effect); 2])> { + pub fn key(&self) -> Option<&(Key, [(u32, Decoration); 2])> { self.key.as_ref() } @@ -113,9 +110,7 @@ impl FormattableText for AccessString { }) } - fn effect_tokens(&self) -> &[(u32, Effect)] { - &[] - } + // Note that we do not display underline decorations by default } impl From for AccessString { diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 9f6e9f44b..0abd6a039 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -14,7 +14,7 @@ use crate::draw::{Draw, DrawIface, DrawRounded, DrawShared, DrawSharedImpl, Imag use crate::event::EventState; #[allow(unused)] use crate::event::{Command, ConfigCx}; use crate::geom::{Coord, Offset, Rect}; -use crate::text::{TextDisplay, format::Effect, format::FormattableText}; +use crate::text::{TextDisplay, format, format::FormattableText}; use crate::theme::ColorsLinear; use crate::{Id, Tile, autoimpl}; #[allow(unused)] use crate::{Layout, theme::TextClass}; @@ -300,7 +300,9 @@ impl<'a> DrawCx<'a> { text: &Text, ) { if let Ok(display) = text.display() { - self.text_with_effects(pos, rect, display, &[], text.effect_tokens()); + let tokens = text.color_tokens(); + self.text_with_effects(pos, rect, display, &[], tokens); + self.decorate_text(pos, rect, display, &[], text.decorations()); } } @@ -313,51 +315,78 @@ impl<'a> DrawCx<'a> { /// 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() { - self.text_with_effects(rect.pos, rect, display, &[color], text.effect_tokens()); + let colors = &[color]; + let tokens = text.color_tokens(); + self.text_with_effects(rect.pos, rect, display, colors, tokens); + self.decorate_text(rect.pos, rect, display, colors, text.decorations()); } } - /// Draw text with a given effect list + /// Draw text with a list of color effects /// /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if /// the text is not scrolled. /// - /// If `colors` is empty, it is replaced with a single theme-defined color. - /// Text is then drawn using `colors[0]` except as specified by effects. + /// If `palette` is empty, it is replaced with a single theme-defined color. + /// Text is then drawn using `palette[0]` except as specified by effects. /// - /// The list of `effects` (if not empty) controls render effects: - /// [`Effect::color`] is an index into `colors` while [`Effect::flags`] controls - /// underline and strikethrough. [`Effect::start`] is the text index at - /// which this effect first takes effect, and must effects must be ordered - /// such that the sequence of [`Effect::start`] values is strictly - /// increasing. [`Effect::default()`] is used if `effects` is empty or while - /// `index < effects.first().unwrap().start`. - /// - /// Text objects may embed their own list of effects, accessible using - /// [`Text::effect_tokens`]. It is always valid to disregard these - /// and use a custom `effects` list or empty list. + /// The list of `tokens` may be the result of [`Text::color_tokens`] or any + /// compatible sequence (including `&[]`). See also + /// [`FormattableText::color_tokens`]. #[inline] pub fn text_with_effects( &mut self, pos: Coord, rect: Rect, display: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, format::Colors)], ) { if cfg!(debug_assertions) { - let num_colors = if colors.is_empty() { 1 } else { colors.len() }; + let num_colors = if palette.is_empty() { 1 } else { palette.len() }; let mut i = 0; - for effect in effects { - assert!(effect.0 >= i); - i = effect.0; + for (start, token) in tokens { + assert!(*start >= i); + i = *start; - assert!(usize::from(effect.1.color) < num_colors); + assert!(usize::from(token.color) < num_colors); } } self.h - .text_effects(&self.id, pos, rect, display, colors, effects); + .text_effects(&self.id, pos, rect, display, palette, tokens); + } + + /// Draw text decorations (e.g. underlines) + /// + /// This does not draw the text itself, but requires most of the same inputs + /// as [`Self::text_with_effects`]. + /// + /// The list of `decorations` may come from [`Text::decorations`] or be any + /// other compatible sequence. See also [`FormattableText::decorations`]. + pub fn decorate_text( + &mut self, + pos: Coord, + rect: Rect, + display: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, format::Decoration)], + ) { + if cfg!(debug_assertions) { + let num_colors = if palette.is_empty() { 1 } else { palette.len() }; + let mut i = 0; + for (start, token) in decorations { + assert!(*start >= i); + i = *start; + + assert!(usize::from(token.color) < num_colors); + } + } + + if !decorations.is_empty() { + self.h + .decorate_text(&self.id, pos, rect, display, palette, decorations); + } } /// Draw some text with a selection @@ -545,21 +574,45 @@ pub trait ThemeDraw { /// Draw a selection highlight / frame fn selection(&mut self, rect: Rect, style: SelectionStyle); - /// Draw text with effects + /// Draw text with a list of color effects /// - /// *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. + /// 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. + /// If `palette` is empty, it is replaced with a single theme-defined color. + /// Text is then drawn using `palette[0]` except as specified by effects. + /// + /// *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_effects( &mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, format::Colors)], + ); + + /// Draw text decorations (e.g. underlines) + /// + /// This does not draw the text itself, but requires most of the same inputs + /// as [`Self::text_effects`]. + /// + /// The list of `decorations` may come from [`Text::decorations`] or be any + /// other compatible sequence. See also [`FormattableText::decorations`]. + fn decorate_text( + &mut self, + id: &Id, + pos: Coord, + rect: Rect, + text: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, format::Decoration)], ); /// Method used to implement [`DrawCx::text_with_selection`] diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 9221c232d..ba037eb3c 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -17,7 +17,7 @@ use crate::dir::{Direction, Directional}; use crate::draw::{color::Rgba, *}; use crate::event::EventState; use crate::geom::*; -use crate::text::{TextDisplay, format::Effect}; +use crate::text::{TextDisplay, format}; use crate::theme::dimensions as dim; use crate::theme::{Background, FrameStyle, MarkStyle}; use crate::theme::{ColorsLinear, InputState, Theme}; @@ -354,22 +354,46 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { pos: Coord, rect: Rect, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + palette: &[Rgba], + tokens: &[(u32, format::Colors)], ) { let bb = Quad::conv(rect); let col; - let mut colors = colors; - if colors.is_empty() { + let mut palette = palette; + if palette.is_empty() { col = [if self.ev.is_disabled(id) { self.cols.text_disabled } else { self.cols.text }]; - colors = &col; + palette = &col; } self.draw - .text_effects(pos.cast(), bb, text, colors, effects); + .text_effects(pos.cast(), bb, text, palette, tokens); + } + + fn decorate_text( + &mut self, + id: &Id, + pos: Coord, + rect: Rect, + text: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, format::Decoration)], + ) { + let bb = Quad::conv(rect); + let col; + let mut palette = palette; + if palette.is_empty() { + col = [if self.ev.is_disabled(id) { + self.cols.text_disabled + } else { + self.cols.text + }]; + palette = &col; + } + self.draw + .decorate_text(pos.cast(), bb, text, palette, decorations); } fn text_selected_range( @@ -398,18 +422,18 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { } }); - let effects = [ + let tokens = [ Default::default(), - (range.start.cast(), Effect { + (range.start.cast(), format::Colors { color: 1, - flags: Default::default(), + ..Default::default() }), - (range.end.cast(), Effect::default()), + (range.end.cast(), format::Colors::default()), ]; let r0 = if range.start > 0 { 0 } else { 1 }; - let colors = [col, sel_col]; + let palette = [col, sel_col]; self.draw - .text_effects(pos, bb, text, &colors, &effects[r0..]); + .text_effects(pos, bb, text, &palette, &tokens[r0..]); } fn text_cursor(&mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay, byte: usize) { diff --git a/crates/kas-core/src/theme/text.rs b/crates/kas-core/src/theme/text.rs index 3eef31bfe..3bd569533 100644 --- a/crates/kas-core/src/theme/text.rs +++ b/crates/kas-core/src/theme/text.rs @@ -15,7 +15,7 @@ use crate::cast::Cast; use crate::geom::{Rect, Vec2}; use crate::layout::{AlignHints, AxisInfo, SizeRules, Stretch}; use crate::text::fonts::FontSelector; -use crate::text::format::{Effect, FormattableText}; +use crate::text::format::{Colors, Decoration, FormattableText}; use crate::text::*; use std::num::NonZeroUsize; @@ -386,17 +386,20 @@ impl Text { is_rtl } - /// Get the sequence of effect tokens + /// Return the sequence of color effect tokens /// - /// This method has some limitations: (1) it may only return a reference to - /// an existing sequence, (2) effect tokens cannot be generated dependent - /// on input state, and (3) it does not incorporate color information. For - /// most uses it should still be sufficient, but for other cases it may be - /// preferable not to use this method (use a dummy implementation returning - /// `&[]` and use inherent methods on the text object via [`Text::text`]). + /// This forwards to [`FormattableText::color_tokens`]. #[inline] - pub fn effect_tokens(&self) -> &[(u32, Effect)] { - self.text.effect_tokens() + 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() } } diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index ea68a2df4..3517581c1 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -86,10 +86,22 @@ impl Methods { pos: Coord, rect: Rect, text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, ::kas::text::format::Effect)], + palette: &[Rgba], + tokens: &[(u32, ::kas::text::format::Colors)], ) { - (#base).text_effects(id, pos, rect, text, colors, effects); + (#base).text_effects(id, pos, rect, text, palette, tokens); + } + + fn decorate_text( + &mut self, + id: &Id, + pos: Coord, + rect: Rect, + text: &TextDisplay, + palette: &[Rgba], + decorations: &[(u32, ::kas::text::format::Decoration)], + ) { + (#base).decorate_text(id, pos, rect, text, palette, decorations); } fn text_selected_range( diff --git a/crates/kas-soft/src/draw.rs b/crates/kas-soft/src/draw.rs index 7b5182ece..497f923a8 100644 --- a/crates/kas-soft/src/draw.rs +++ b/crates/kas-soft/src/draw.rs @@ -180,13 +180,13 @@ impl DrawSharedImpl for Shared { fn draw_text_effects( &mut self, - draw: &mut Draw, + draw: &mut Self::Draw, pass: PassId, pos: Vec2, bb: Quad, text: &text::TextDisplay, - colors: &[color::Rgba], - effects: &[(u32, text::format::Effect)], + palette: &[color::Rgba], + tokens: &[(u32, text::format::Colors)], ) { let time = std::time::Instant::now(); self.text.text_effects( @@ -196,12 +196,30 @@ impl DrawSharedImpl for Shared { pos, bb, text, - colors, - effects, + palette, + tokens, |quad, col| { draw.basic.rect(pass, quad, col); }, ); draw.common.report_dur_text(time.elapsed()); } + + fn decorate_text( + &mut self, + draw: &mut Self::Draw, + pass: PassId, + pos: Vec2, + bb: Quad, + text: &text::TextDisplay, + palette: &[color::Rgba], + decorations: &[(u32, text::format::Decoration)], + ) { + let time = std::time::Instant::now(); + self.text + .decorate_text(pos, bb, text, palette, decorations, |quad, col| { + draw.basic.rect(pass, quad, col); + }); + draw.common.report_dur_text(time.elapsed()); + } } diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 465fe4c49..c4e6921df 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -18,7 +18,7 @@ use kas::draw::color::Rgba; use kas::draw::*; use kas::geom::{Quad, Size, Vec2}; use kas::runner::{GraphicsFeatures, RunError}; -use kas::text::{TextDisplay, format::Effect}; +use kas::text; impl DrawPipe { /// Construct @@ -362,9 +362,9 @@ impl DrawSharedImpl for DrawPipe { pass: PassId, pos: Vec2, bb: Quad, - text: &TextDisplay, - colors: &[Rgba], - effects: &[(u32, Effect)], + text: &text::TextDisplay, + palette: &[color::Rgba], + tokens: &[(u32, text::format::Colors)], ) { let time = std::time::Instant::now(); self.text.text_effects( @@ -374,14 +374,32 @@ impl DrawSharedImpl for DrawPipe { pos, bb, text, - colors, - effects, + palette, + tokens, |quad, col| { draw.shaded_square.rect(pass, quad, col); }, ); draw.common.report_dur_text(time.elapsed()); } + + fn decorate_text( + &mut self, + draw: &mut Self::Draw, + pass: PassId, + pos: Vec2, + bb: Quad, + text: &text::TextDisplay, + palette: &[color::Rgba], + decorations: &[(u32, text::format::Decoration)], + ) { + let time = std::time::Instant::now(); + self.text + .decorate_text(pos, bb, text, palette, decorations, |quad, col| { + draw.shaded_square.rect(pass, quad, col); + }); + draw.common.report_dur_text(time.elapsed()); + } } impl DrawImpl for DrawWindow { diff --git a/crates/kas-widgets/src/access_label.rs b/crates/kas-widgets/src/access_label.rs index 18f216b64..f880609f4 100644 --- a/crates/kas-widgets/src/access_label.rs +++ b/crates/kas-widgets/src/access_label.rs @@ -130,15 +130,14 @@ mod AccessLabel { fn draw(&self, mut draw: DrawCx) { let rect = self.text.rect(); - if let Some((key, effects)) = self.text.text().key() + draw.text(rect, &self.text); + 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() { - draw.text_with_effects(rect.pos, rect, display, &[], effects); + draw.decorate_text(rect.pos, rect, display, &[], decoration); } - } else { - draw.text(rect, &self.text); } } } diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index e18c9c7ec..0fcce9f5f 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -10,8 +10,7 @@ use kas::event::components::{TextInput, TextInputAction}; use kas::event::{ElementState, FocusSource, Ime, ImePurpose, ImeSurroundingText, Scroll}; use kas::geom::Vec2; use kas::prelude::*; -use kas::text::format::{Effect, EffectFlags}; -use kas::text::{CursorRange, NotReady, SelectionHelper}; +use kas::text::{CursorRange, NotReady, SelectionHelper, format}; use kas::theme::{Text, TextClass}; use kas::util::UndoStack; use std::borrow::Cow; @@ -164,22 +163,21 @@ impl Component { pub fn draw_with_offset(&self, mut draw: DrawCx, rect: Rect, offset: Offset) { let pos = self.rect().pos - offset; + draw.text_with_selection(pos, rect, &self.text, self.selection.range()); + if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { - // TODO: combine underline with selection highlight - let effects = [ + let tokens = [ Default::default(), - (edit_range.start, Effect { - color: 0, - flags: EffectFlags::UNDERLINE, + (edit_range.start, format::Decoration { + dec: format::DecorationType::Underline, + ..Default::default() }), - (edit_range.end, Effect::default()), + (edit_range.end, Default::default()), ]; let r0 = if edit_range.start > 0 { 0 } else { 1 }; if let Ok(display) = self.text.display() { - draw.text_with_effects(pos, rect, display, &[], &effects[r0..]); + draw.decorate_text(pos, rect, display, &[], &tokens[r0..]); } - } else { - draw.text_with_selection(pos, rect, &self.text, self.selection.range()); } if self.editable && draw.ev_state().has_input_focus(self.id_ref()) == Some(true) { From d4bbe4cb7972f59c932469ace9f7edbc8ec3e2a3 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 19 Feb 2026 07:17:51 +0000 Subject: [PATCH 08/14] Import Markdown parser from kas-text --- crates/kas-core/Cargo.toml | 3 +- crates/kas-core/src/text/format.rs | 3 + crates/kas-core/src/text/format/markdown.rs | 395 ++++++++++++++++++++ 3 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 crates/kas-core/src/text/format/markdown.rs diff --git a/crates/kas-core/Cargo.toml b/crates/kas-core/Cargo.toml index 3ebfcc27f..a2977930b 100644 --- a/crates/kas-core/Cargo.toml +++ b/crates/kas-core/Cargo.toml @@ -49,7 +49,7 @@ ab_glyph = ["kas-text/ab_glyph", "dep:ab_glyph"] shaping = ["kas-text/shaping"] # Enable Markdown parsing -markdown = ["kas-text/markdown"] +markdown = ["pulldown-cmark"] # Enable support for YAML (de)serialisation yaml = ["serde", "dep:serde_yaml2"] @@ -116,6 +116,7 @@ swash = { version = "0.2.4", features = ["scale"] } linearize = { version = "0.1.5", features = ["derive"] } kas-text = "0.9.0" easy-cast = "0.5.4" # used in doc links +pulldown-cmark = { version = "0.13.0", optional = true } [dependencies.kas-macros] version = "=0.17.0" # pinned because kas-macros makes assumptions about kas-core's internals diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index 55b7b09b4..bcc60ea29 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -8,6 +8,9 @@ use super::fonts::FontSelector; pub use kas_text::format::FontToken; +#[cfg(feature = "markdown")] mod markdown; +#[cfg(feature = "markdown")] pub use markdown::Markdown; + /// Effect formatting marker: text and background color #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/kas-core/src/text/format/markdown.rs b/crates/kas-core/src/text/format/markdown.rs new file mode 100644 index 000000000..efd8e9d94 --- /dev/null +++ b/crates/kas-core/src/text/format/markdown.rs @@ -0,0 +1,395 @@ +// 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 + +//! Markdown parsing + +use super::{Decoration, DecorationType, FontToken, FormattableText}; +use crate::cast::Cast; +use crate::text::fonts::{FamilySelector, FontSelector, FontStyle, FontWeight}; +use pulldown_cmark::{Event, HeadingLevel, Tag, TagEnd}; +use std::fmt::Write; +use std::iter::FusedIterator; +use thiserror::Error; + +/// Markdown parsing errors +#[derive(Error, Debug)] +pub enum Error { + #[error("Not supported by Markdown parser: {0}")] + NotSupported(&'static str), +} + +/// Basic Markdown formatter +/// +/// Currently this misses several important Markdown features, but may still +/// prove a convenient way of constructing formatted texts. +/// +/// Supported: +/// +/// - Text paragraphs +/// - Code (embedded and blocks); caveat: extra line after code blocks +/// - Explicit line breaks +/// - Headings +/// - Lists (numerated and bulleted); caveat: indentation after first line +/// - Bold, italic (emphasis), strike-through +/// +/// Not supported: +/// +/// - Block quotes +/// - Footnotes +/// - HTML +/// - Horizontal rules +/// - Images +/// - Links +/// - Tables +/// - Task lists +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Markdown { + text: String, + fmt: Vec, + decorations: Vec<(u32, Decoration)>, +} + +impl Markdown { + /// Parse the input as Markdown + /// + /// Parsing happens immediately. Fonts must be initialized before calling + /// this method. + #[inline] + pub fn new(input: &str) -> Result { + parse(input) + } +} + +pub struct FontTokenIter<'a> { + index: usize, + fmt: &'a [Fmt], + base_dpem: f32, + base_font: FontSelector, +} + +impl<'a> FontTokenIter<'a> { + fn new(fmt: &'a [Fmt], base_dpem: f32, base_font: FontSelector) -> Self { + FontTokenIter { + index: 0, + fmt, + base_dpem, + base_font, + } + } +} + +impl<'a> Iterator for FontTokenIter<'a> { + type Item = FontToken; + + fn next(&mut self) -> Option { + if self.index < self.fmt.len() { + let fmt = &self.fmt[self.index]; + self.index += 1; + let start = fmt.start; + let dpem = self.base_dpem * fmt.rel_size; + + let mut font = self.base_font; + if fmt.bold { + font.weight = FontWeight::BOLD; + } + if fmt.italic { + font.style = FontStyle::Italic; + } + if fmt.monospace { + font.family = FamilySelector::MONOSPACE; + } + Some(FontToken { start, font, dpem }) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.fmt.len(); + (len, Some(len)) + } +} + +impl<'a> ExactSizeIterator for FontTokenIter<'a> {} +impl<'a> FusedIterator for FontTokenIter<'a> {} + +impl FormattableText for Markdown { + #[inline] + fn as_str(&self) -> &str { + &self.text + } + + #[inline] + fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator { + FontTokenIter::new(&self.fmt, dpem, font) + } + + #[inline] + fn decorations(&self) -> &[(u32, Decoration)] { + &self.decorations + } +} + +fn parse(input: &str) -> Result { + let mut text = String::with_capacity(input.len()); + let mut fmt: Vec = vec![Fmt::default()]; + let mut set_last = |item: &StackItem| { + let f = item.fmt.clone(); + if let Some(last) = fmt.last_mut() + && last.start >= item.fmt.start + { + *last = f; + return; + } + fmt.push(f); + }; + + let mut state = State::None; + let mut stack = Vec::with_capacity(16); + let mut item = StackItem::default(); + + let options = pulldown_cmark::Options::ENABLE_STRIKETHROUGH; + for ev in pulldown_cmark::Parser::new_ext(input, options) { + match ev { + Event::Start(tag) => { + item.fmt.start = text.len().cast(); + if let Some(clone) = item.start_tag(&mut text, &mut state, tag)? { + stack.push(item); + item = clone; + set_last(&item); + } + } + Event::End(tag) => { + if item.end_tag(&mut state, tag) { + item = stack.pop().unwrap(); + item.fmt.start = text.len().cast(); + set_last(&item); + } + } + Event::Text(part) => { + state.part(&mut text); + text.push_str(&part); + } + Event::Code(part) => { + state.part(&mut text); + item.fmt.start = text.len().cast(); + + let mut item2 = item.clone(); + item2.fmt.monospace = true; + set_last(&item2); + + text.push_str(&part); + + item.fmt.start = text.len().cast(); + set_last(&item); + } + Event::InlineMath(_) | Event::DisplayMath(_) => { + return Err(Error::NotSupported("math expressions")); + } + Event::Html(_) | Event::InlineHtml(_) => { + return Err(Error::NotSupported("embedded HTML")); + } + Event::FootnoteReference(_) => return Err(Error::NotSupported("footnote")), + Event::SoftBreak => state.soft_break(&mut text), + Event::HardBreak => state.hard_break(&mut text), + Event::Rule => return Err(Error::NotSupported("horizontal rule")), + Event::TaskListMarker(_) => return Err(Error::NotSupported("task list")), + } + } + + // TODO(opt): don't need to store flags in fmt? + let mut decorations = Vec::new(); + let mut strikethrough = false; + for token in &fmt { + if token.strikethrough != strikethrough { + let mut dec = Decoration::default(); + if token.strikethrough { + dec.dec = DecorationType::Strikethrough; + } + decorations.push((token.start, dec)); + strikethrough = token.strikethrough; + } + } + + Ok(Markdown { + text, + fmt, + decorations, + }) +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum State { + None, + BlockStart, + BlockEnd, + ListItem, + Part, +} + +impl State { + fn start_block(&mut self, text: &mut String) { + match *self { + State::None | State::BlockStart => (), + State::BlockEnd | State::ListItem | State::Part => text.push_str("\n\n"), + } + *self = State::BlockStart; + } + fn end_block(&mut self) { + *self = State::BlockEnd; + } + fn part(&mut self, text: &mut String) { + match *self { + State::None | State::BlockStart | State::Part | State::ListItem => (), + State::BlockEnd => text.push_str("\n\n"), + } + *self = State::Part; + } + fn list_item(&mut self, text: &mut String) { + match *self { + State::None | State::BlockStart | State::BlockEnd => { + debug_assert_eq!(*self, State::BlockStart); + } + State::ListItem | State::Part => text.push('\n'), + } + *self = State::ListItem; + } + fn soft_break(&mut self, text: &mut String) { + text.push(' '); + } + fn hard_break(&mut self, text: &mut String) { + text.push('\n'); + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Fmt { + start: u32, + rel_size: f32, + bold: bool, + italic: bool, + monospace: bool, + strikethrough: bool, +} + +impl Default for Fmt { + fn default() -> Self { + Fmt { + start: 0, + rel_size: 1.0, + bold: false, + italic: false, + monospace: false, + strikethrough: false, + } + } +} + +#[derive(Clone, Debug, Default)] +struct StackItem { + list: Option, + fmt: Fmt, +} + +impl StackItem { + // process a tag; may modify current item and may return new item + fn start_tag( + &mut self, + text: &mut String, + state: &mut State, + tag: Tag, + ) -> Result, Error> { + fn with_clone(s: &mut StackItem, c: F) -> Option { + let mut item = s.clone(); + c(&mut item); + Some(item) + } + + Ok(match tag { + Tag::Paragraph => { + state.start_block(text); + None + } + Tag::Heading { level, .. } => { + state.start_block(text); + self.fmt.start = text.len().cast(); + with_clone(self, |item| { + // CSS sizes: https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-size-prop + item.fmt.rel_size = match level { + HeadingLevel::H1 => 2.0 / 1.0, + HeadingLevel::H2 => 3.0 / 2.0, + HeadingLevel::H3 => 6.0 / 5.0, + HeadingLevel::H4 => 1.0, + HeadingLevel::H5 => 8.0 / 9.0, + HeadingLevel::H6 => 3.0 / 5.0, + } + }) + } + Tag::CodeBlock(_) => { + state.start_block(text); + self.fmt.start = text.len().cast(); + with_clone(self, |item| { + item.fmt.monospace = true; + }) + // TODO: within a code block, the last \n should be suppressed? + } + Tag::HtmlBlock => return Err(Error::NotSupported("embedded HTML")), + Tag::List(start) => { + state.start_block(text); + self.list = start; + None + } + Tag::Item => { + state.list_item(text); + // NOTE: we use \t for indent, which indents only the first + // line. Without better flow control we cannot fix this. + match &mut self.list { + Some(x) => { + write!(text, "{x}\t").unwrap(); + *x += 1; + } + None => text.push_str("•\t"), + } + None + } + Tag::Emphasis => with_clone(self, |item| item.fmt.italic = true), + Tag::Strong => with_clone(self, |item| item.fmt.bold = true), + Tag::Strikethrough => with_clone(self, |item| { + item.fmt.strikethrough = true; + }), + Tag::BlockQuote(_) => return Err(Error::NotSupported("block quote")), + Tag::FootnoteDefinition(_) => return Err(Error::NotSupported("footnote")), + Tag::DefinitionList | Tag::DefinitionListTitle | Tag::DefinitionListDefinition => { + return Err(Error::NotSupported("definition")); + } + Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => { + return Err(Error::NotSupported("table")); + } + Tag::Superscript | Tag::Subscript => { + // kas-text doesn't support adjusting the baseline + return Err(Error::NotSupported("super/subscript")); + } + Tag::Link { .. } => return Err(Error::NotSupported("link")), + Tag::Image { .. } => return Err(Error::NotSupported("image")), + Tag::MetadataBlock(_) => return Err(Error::NotSupported("metadata block")), + }) + } + // returns true if stack must be popped + fn end_tag(&self, state: &mut State, tag: TagEnd) -> bool { + match tag { + TagEnd::Paragraph | TagEnd::List(_) => { + state.end_block(); + false + } + TagEnd::Heading(_) | TagEnd::CodeBlock => { + state.end_block(); + true + } + TagEnd::Item => false, + TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => true, + tag => unimplemented!("{:?}", tag), + } + } +} From 9361f5e30ced200e487f0041fd08ef4a15b8cffd Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 18 Feb 2026 13:51:47 +0000 Subject: [PATCH 09/14] Add kas::text::format::Color type --- crates/kas-core/src/draw/draw.rs | 31 +++++++-- crates/kas-core/src/draw/draw_shared.rs | 3 + crates/kas-core/src/text/format.rs | 83 +++++++++++++++++++++-- crates/kas-core/src/text/raster.rs | 19 +++--- crates/kas-core/src/theme/draw.rs | 8 +-- crates/kas-core/src/theme/simple_theme.rs | 31 ++------- crates/kas-soft/src/draw.rs | 6 +- crates/kas-wgpu/src/draw/draw_pipe.rs | 6 +- 8 files changed, 131 insertions(+), 56 deletions(-) diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index f334ec00d..8a9140cfb 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -10,6 +10,7 @@ use super::{AnimationState, color::Rgba}; use super::{DrawShared, DrawSharedImpl, ImageId, PassId, PassType, SharedState, WindowCommon}; use crate::geom::{Offset, Quad, Rect, Vec2}; use crate::text::{TextDisplay, format}; +use crate::theme::ColorsLinear; use std::any::Any; use std::time::Instant; @@ -127,8 +128,15 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> { /// /// The `text` display must be prepared prior to calling this method. /// Typically this is done using a [`crate::theme::Text`] object. - pub fn text(&mut self, pos: Vec2, bounding_box: Quad, text: &TextDisplay, col: Rgba) { - self.text_effects(pos, bounding_box, text, &[col], &[]); + pub fn text( + &mut self, + pos: Vec2, + bounding_box: Quad, + text: &TextDisplay, + theme: &ColorsLinear, + col: Rgba, + ) { + self.text_effects(pos, bounding_box, text, theme, &[col], &[]); } } @@ -234,6 +242,7 @@ pub trait Draw { pos: Vec2, bounding_box: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], tokens: &[(u32, format::Colors)], ); @@ -247,6 +256,7 @@ pub trait Draw { pos: Vec2, bounding_box: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], decorations: &[(u32, format::Decoration)], ); @@ -305,12 +315,13 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { pos: Vec2, bb: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], tokens: &[(u32, format::Colors)], ) { self.shared .draw - .draw_text_effects(self.draw, self.pass, pos, bb, text, palette, tokens); + .draw_text_effects(self.draw, self.pass, pos, bb, text, theme, palette, tokens); } fn decorate_text( @@ -318,12 +329,20 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { pos: Vec2, bb: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], decorations: &[(u32, format::Decoration)], ) { - self.shared - .draw - .decorate_text(self.draw, self.pass, pos, bb, text, palette, decorations); + self.shared.draw.decorate_text( + self.draw, + self.pass, + pos, + bb, + text, + theme, + palette, + decorations, + ); } } diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index 9d3e9cf6a..b7ce36ce7 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -11,6 +11,7 @@ use crate::ActionRedraw; use crate::config::RasterConfig; use crate::geom::{Quad, Size, Vec2}; use crate::text::{TextDisplay, format}; +use crate::theme::ColorsLinear; use std::any::Any; use std::num::NonZeroU32; use std::sync::Arc; @@ -214,6 +215,7 @@ pub trait DrawSharedImpl: Any { pos: Vec2, bounding_box: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], tokens: &[(u32, format::Colors)], ); @@ -229,6 +231,7 @@ pub trait DrawSharedImpl: Any { pos: Vec2, bounding_box: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], decorations: &[(u32, format::Decoration)], ); diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index bcc60ea29..faaadcc6b 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -5,21 +5,92 @@ //! Formatted text traits and types +use std::num::NonZeroU16; + use super::fonts::FontSelector; +use crate::{draw::color::Rgba, theme::ColorsLinear}; pub use kas_text::format::FontToken; #[cfg(feature = "markdown")] mod markdown; #[cfg(feature = "markdown")] pub use markdown::Markdown; +/// Palette or theme-provided color value +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Color(NonZeroU16); + +impl Default for Color { + /// Use a theme-defined color (automatic) + /// + /// For backgrounds, this uses the text-selection color. + fn default() -> Self { + Color(NonZeroU16::new(1).unwrap()) + } +} + +impl Color { + /// Use a color palette index + /// + /// Returns `None` if `index >= 0x8000`. + #[inline] + pub fn from_index(index: usize) -> Option { + (index < 0x8000) + .then_some(NonZeroU16::new(0x8000 | (index as u16)).map(Color)) + .flatten() + } + + /// Get the palette index, if any + #[inline] + pub fn as_index(self) -> Option { + let color = self.0.get(); + (color & 0x8000 != 0).then_some((color & 0x7FFF) as usize) + } + + /// Validate index values against a given palette + #[cfg(debug_assertions)] + pub fn validate(self, palette: &[Rgba]) { + if let Some(index) = self.as_index() { + assert!( + index < palette.len(), + "text::format::Color: invalid palette index" + ); + } + } + + /// Resolve color from the palette + /// + /// This returns `None` if either this does not represent a palette index or + /// the represented index is not present in the palette. + #[inline] + pub fn resolve_palette_color(self, palette: &[Rgba]) -> Option { + self.as_index() + .and_then(|index| palette.get(index).cloned()) + } + + /// Resolve color + /// + /// If this represents a valid palette index, the palette's color will be + /// used, otherwise the color will be inferred from the theme, using `bg` + /// if provided. + #[inline] + pub fn resolve_color(self, theme: &ColorsLinear, palette: &[Rgba], bg: Option) -> Rgba { + if let Some(col) = self.resolve_palette_color(palette) { + col + } else if let Some(bg) = bg { + theme.text_over(bg) + } else { + theme.text + } + } +} + /// Effect formatting marker: text and background color +/// +/// By default, this uses the theme's text color without a background. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Colors { - /// User-specified value - /// - /// Usage is not specified by `kas-text`, but typically this field will be - /// used as an index into a colour palette or not used at all. - pub color: u16, + pub color: Color, } /// Decoration types @@ -53,7 +124,7 @@ pub struct Decoration { /// Line style pub style: LineStyle, /// Line color - pub color: u16, + pub color: Color, } /// Text, optionally with formatting data diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index 53d860228..d19d46c93 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -11,7 +11,8 @@ //! Text drawing pipeline use crate::config::SubpixelMode; -use crate::text::format::{Colors, Decoration, DecorationType, LineStyle}; +use crate::text::format::{Color, Colors, Decoration, DecorationType, LineStyle}; +use crate::theme::ColorsLinear; use kas::cast::traits::*; use kas::config::RasterConfig; use kas::draw::{AllocError, Allocation, PassId, color::Rgba}; @@ -542,6 +543,7 @@ impl State { pos: Vec2, bb: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], tokens: &[(u32, Colors)], mut draw_quad: impl FnMut(Quad, Rgba), @@ -553,7 +555,7 @@ impl State { .map(|e| e.1 == Default::default()) .unwrap_or(true) { - let col = palette.first().cloned().unwrap_or(Rgba::BLACK); + let col = Color::default().resolve_color(theme, palette, None); self.text(allocator, queue, pass, pos, bb, text, col); return; } @@ -573,10 +575,8 @@ impl State { } } }; - let col = palette - .get(usize::conv(token.color)) - .cloned() - .unwrap_or(Rgba::BLACK); + + let col = token.color.resolve_color(theme, palette, None); queue.push_sprite(pass, glyph.position.into(), bb, col, sprite); }; @@ -594,6 +594,7 @@ impl State { pos: Vec2, bb: Quad, text: &TextDisplay, + theme: &ColorsLinear, palette: &[Rgba], tokens: &[(u32, Decoration)], mut draw_quad: impl FnMut(Quad, Rgba), @@ -624,10 +625,8 @@ impl State { return; }; - let col = palette - .get(usize::conv(token.color)) - .cloned() - .unwrap_or(Rgba::BLACK); + // Known limitation: this cannot depend on the background color. + let col = token.color.resolve_color(theme, palette, None); match token.style { LineStyle::Solid => draw_quad(quad, col), diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 0abd6a039..688ca4e5e 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -343,13 +343,11 @@ impl<'a> DrawCx<'a> { tokens: &[(u32, format::Colors)], ) { if cfg!(debug_assertions) { - let num_colors = if palette.is_empty() { 1 } else { palette.len() }; let mut i = 0; for (start, token) in tokens { assert!(*start >= i); i = *start; - - assert!(usize::from(token.color) < num_colors); + token.color.validate(palette); } } @@ -373,13 +371,11 @@ impl<'a> DrawCx<'a> { decorations: &[(u32, format::Decoration)], ) { if cfg!(debug_assertions) { - let num_colors = if palette.is_empty() { 1 } else { palette.len() }; let mut i = 0; for (start, token) in decorations { assert!(*start >= i); i = *start; - - assert!(usize::from(token.color) < num_colors); + token.color.validate(palette); } } diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index ba037eb3c..c51b44e16 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -10,6 +10,7 @@ use std::f32; use std::ops::Range; use std::time::Instant; +use super::ColorsSrgb; use crate::Id; use crate::cast::traits::*; use crate::config::{Config, WindowConfig}; @@ -23,8 +24,6 @@ use crate::theme::{Background, FrameStyle, MarkStyle}; use crate::theme::{ColorsLinear, InputState, Theme}; use crate::theme::{SelectionStyle, ThemeDraw, ThemeSize}; -use super::ColorsSrgb; - /// A simple theme /// /// This theme is functional, but not pretty. It is intended as a template for @@ -358,18 +357,8 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { tokens: &[(u32, format::Colors)], ) { let bb = Quad::conv(rect); - let col; - let mut palette = palette; - if palette.is_empty() { - col = [if self.ev.is_disabled(id) { - self.cols.text_disabled - } else { - self.cols.text - }]; - palette = &col; - } self.draw - .text_effects(pos.cast(), bb, text, palette, tokens); + .text_effects(pos.cast(), bb, text, self.cols, palette, tokens); } fn decorate_text( @@ -382,18 +371,8 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { decorations: &[(u32, format::Decoration)], ) { let bb = Quad::conv(rect); - let col; - let mut palette = palette; - if palette.is_empty() { - col = [if self.ev.is_disabled(id) { - self.cols.text_disabled - } else { - self.cols.text - }]; - palette = &col; - } self.draw - .decorate_text(pos.cast(), bb, text, palette, decorations); + .decorate_text(pos.cast(), bb, text, self.cols, palette, decorations); } fn text_selected_range( @@ -425,7 +404,7 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { let tokens = [ Default::default(), (range.start.cast(), format::Colors { - color: 1, + color: format::Color::from_index(1).unwrap(), ..Default::default() }), (range.end.cast(), format::Colors::default()), @@ -433,7 +412,7 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { let r0 = if range.start > 0 { 0 } else { 1 }; let palette = [col, sel_col]; self.draw - .text_effects(pos, bb, text, &palette, &tokens[r0..]); + .text_effects(pos, bb, text, self.cols, &palette, &tokens[r0..]); } fn text_cursor(&mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay, byte: usize) { diff --git a/crates/kas-soft/src/draw.rs b/crates/kas-soft/src/draw.rs index 497f923a8..cb6368220 100644 --- a/crates/kas-soft/src/draw.rs +++ b/crates/kas-soft/src/draw.rs @@ -14,6 +14,7 @@ use kas::draw::{ImageFormat, ImageId, color}; use kas::geom::{Quad, Size, Vec2}; use kas::prelude::{Offset, Rect}; use kas::text::{self}; +use kas::theme::ColorsLinear; #[derive(Debug)] struct ClipRegion { @@ -185,6 +186,7 @@ impl DrawSharedImpl for Shared { pos: Vec2, bb: Quad, text: &text::TextDisplay, + theme: &ColorsLinear, palette: &[color::Rgba], tokens: &[(u32, text::format::Colors)], ) { @@ -196,6 +198,7 @@ impl DrawSharedImpl for Shared { pos, bb, text, + theme, palette, tokens, |quad, col| { @@ -212,12 +215,13 @@ impl DrawSharedImpl for Shared { pos: Vec2, bb: Quad, text: &text::TextDisplay, + theme: &ColorsLinear, palette: &[color::Rgba], decorations: &[(u32, text::format::Decoration)], ) { let time = std::time::Instant::now(); self.text - .decorate_text(pos, bb, text, palette, decorations, |quad, col| { + .decorate_text(pos, bb, text, theme, palette, decorations, |quad, col| { draw.basic.rect(pass, quad, col); }); draw.common.report_dur_text(time.elapsed()); diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index c4e6921df..1865450ae 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -6,6 +6,7 @@ //! Drawing API for `kas_wgpu` use futures_lite::future::block_on; +use kas::theme::ColorsLinear; use std::f32::consts::FRAC_PI_2; use wgpu::util::DeviceExt; @@ -363,6 +364,7 @@ impl DrawSharedImpl for DrawPipe { pos: Vec2, bb: Quad, text: &text::TextDisplay, + theme: &ColorsLinear, palette: &[color::Rgba], tokens: &[(u32, text::format::Colors)], ) { @@ -374,6 +376,7 @@ impl DrawSharedImpl for DrawPipe { pos, bb, text, + theme, palette, tokens, |quad, col| { @@ -390,12 +393,13 @@ impl DrawSharedImpl for DrawPipe { pos: Vec2, bb: Quad, text: &text::TextDisplay, + theme: &ColorsLinear, palette: &[color::Rgba], decorations: &[(u32, text::format::Decoration)], ) { let time = std::time::Instant::now(); self.text - .decorate_text(pos, bb, text, palette, decorations, |quad, col| { + .decorate_text(pos, bb, text, theme, palette, decorations, |quad, col| { draw.shaded_square.rect(pass, quad, col); }); draw.common.report_dur_text(time.elapsed()); From 38cf548a63ea18bf96c7fbfd786361f7d32beb98 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 19 Feb 2026 08:36:38 +0000 Subject: [PATCH 10/14] Note that Colors::text_disabled is currently unused --- crates/kas-core/src/theme/colors.rs | 2 ++ crates/kas-core/src/theme/simple_theme.rs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/crates/kas-core/src/theme/colors.rs b/crates/kas-core/src/theme/colors.rs index 0c2f49211..2c73a2845 100644 --- a/crates/kas-core/src/theme/colors.rs +++ b/crates/kas-core/src/theme/colors.rs @@ -156,6 +156,8 @@ pub struct Colors { /// Opposing text colour (e.g. white if `text` is black) pub text_invert: C, /// Disabled text colour + /// + /// NOTE: this is not currently used. pub text_disabled: C, /// Selected text background colour /// diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index c51b44e16..5a486d67a 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -356,6 +356,10 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { palette: &[Rgba], tokens: &[(u32, format::Colors)], ) { + // NOTE: id is passed to allow usage of self.cols.text_disabled if self.ev.is_disabled(id). + // We could do this by passing the pair (text, text_invert) instead of self.cols. + let _ = id; + let bb = Quad::conv(rect); self.draw .text_effects(pos.cast(), bb, text, self.cols, palette, tokens); @@ -370,6 +374,9 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { palette: &[Rgba], decorations: &[(u32, format::Decoration)], ) { + // NOTE: see above note on usage of self.cols.text_disabled. + let _ = id; + let bb = Quad::conv(rect); self.draw .decorate_text(pos.cast(), bb, text, self.cols, palette, decorations); From f86b913a9789f06cb75308727ec6af292a93a87f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 18 Feb 2026 13:51:47 +0000 Subject: [PATCH 11/14] Support text background colors as an effect; replace fn DrawCx::text_with_selection --- crates/kas-core/src/text/format.rs | 13 ++++++- crates/kas-core/src/text/raster.rs | 13 +++++-- crates/kas-core/src/theme/draw.rs | 37 ++------------------ crates/kas-core/src/theme/flat_theme.rs | 1 - crates/kas-core/src/theme/simple_theme.rs | 41 ----------------------- crates/kas-macros/src/extends.rs | 11 ------ crates/kas-wgpu/src/shaded_theme.rs | 1 - crates/kas-widgets/src/edit/editor.rs | 21 +++++++++--- crates/kas-widgets/src/scroll_label.rs | 23 ++++++++++--- 9 files changed, 60 insertions(+), 101 deletions(-) diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index faaadcc6b..994fe7c7f 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -91,6 +91,17 @@ impl Color { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Colors { pub color: Color, + pub background: Option, +} + +impl Colors { + /// Resolve the background color, if any + pub fn resolve_background_color(self, theme: &ColorsLinear, palette: &[Rgba]) -> Option { + self.background.map(|bg| { + bg.resolve_palette_color(palette) + .unwrap_or(theme.text_sel_bg) + }) + } } /// Decoration types @@ -239,7 +250,7 @@ impl FormattableText for &F { fn sizes() { use std::mem::size_of; - assert_eq!(size_of::(), 2); + assert_eq!(size_of::(), 4); assert_eq!(size_of::(), 1); assert_eq!(size_of::(), 0); assert_eq!(size_of::(), 4); diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index d19d46c93..93ea11ff7 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -580,8 +580,17 @@ impl State { queue.push_sprite(pass, glyph.position.into(), bb, col, sprite); }; - let sf = run.scaled_face(); - let for_range = |p: kas_text::Vec2, x2, token: Colors| {}; + let for_range = |p: kas_text::Vec2, x2, colors: Colors| { + let Some(col) = colors.resolve_background_color(theme, palette) else { + return; + }; + + let a = Vec2(p.0, run.line_top()); + let b = Vec2(x2, run.line_bottom()); + if let Some(quad) = Quad::from_coords(a, b).intersection(&bb) { + draw_quad(quad, col); + } + }; run.glyphs_with_effects(for_glyph, for_range); } diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 688ca4e5e..d3b885698 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -18,7 +18,6 @@ use crate::text::{TextDisplay, format, format::FormattableText}; use crate::theme::ColorsLinear; use crate::{Id, Tile, autoimpl}; #[allow(unused)] use crate::{Layout, theme::TextClass}; -use std::ops::Range; use std::time::Instant; /// Optional background colour @@ -348,6 +347,7 @@ impl<'a> DrawCx<'a> { assert!(*start >= i); i = *start; token.color.validate(palette); + token.background.map(|bg| bg.validate(palette)); } } @@ -385,29 +385,6 @@ impl<'a> DrawCx<'a> { } } - /// Draw some text with a selection - /// - /// Text is drawn like [`Self::text_with_position`] except that the subset - /// identified by `range` is highlighted using theme-defined colors. - pub fn text_with_selection( - &mut self, - pos: Coord, - rect: Rect, - text: &Text, - range: Range, - ) { - if range.is_empty() { - return self.text_with_position(pos, rect, text); - } - - let Ok(display) = text.display() else { - return; - }; - - self.h - .text_selected_range(&self.id, pos, rect, display, range); - } - /// Draw an edit marker at the given `byte` index on this `text` /// /// The text cursor is draw from `rect.pos` and clipped to `rect`. @@ -611,16 +588,6 @@ pub trait ThemeDraw { decorations: &[(u32, format::Decoration)], ); - /// Method used to implement [`DrawCx::text_with_selection`] - fn text_selected_range( - &mut self, - id: &Id, - pos: Coord, - rect: Rect, - text: &TextDisplay, - range: Range, - ); - /// Draw an edit marker at the given `byte` index on this `text` /// /// The `text` should be prepared before calling this method. @@ -690,6 +657,6 @@ mod test { let _scale = draw.size_cx().scale_factor(); let text = crate::theme::Text::new("sample", TextClass::Label, false); - draw.text_with_selection(Coord::ZERO, Rect::ZERO, &text, 0..6) + draw.text(Rect::ZERO, &text) } } diff --git a/crates/kas-core/src/theme/flat_theme.rs b/crates/kas-core/src/theme/flat_theme.rs index a332b7c1e..b9c934ce5 100644 --- a/crates/kas-core/src/theme/flat_theme.rs +++ b/crates/kas-core/src/theme/flat_theme.rs @@ -7,7 +7,6 @@ use std::cell::RefCell; use std::f32; -use std::ops::Range; use std::time::Instant; use super::SimpleTheme; diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 5a486d67a..7d250adf6 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -7,7 +7,6 @@ use std::cell::RefCell; use std::f32; -use std::ops::Range; use std::time::Instant; use super::ColorsSrgb; @@ -382,46 +381,6 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { .decorate_text(pos.cast(), bb, text, self.cols, palette, decorations); } - fn text_selected_range( - &mut self, - id: &Id, - pos: Coord, - rect: Rect, - text: &TextDisplay, - range: Range, - ) { - let pos = pos.cast(); - let bb = Quad::conv(rect); - let col = if self.ev.is_disabled(id) { - self.cols.text_disabled - } else { - self.cols.text - }; - let sel_col = self.cols.text_over(self.cols.text_sel_bg); - - // Draw background: - text.highlight_range(range.clone(), &mut |p1, p2| { - let p1 = Vec2::from(p1); - let p2 = Vec2::from(p2); - if let Some(quad) = Quad::from_coords(pos + p1, pos + p2).intersection(&bb) { - self.draw.rect(quad, self.cols.text_sel_bg); - } - }); - - let tokens = [ - Default::default(), - (range.start.cast(), format::Colors { - color: format::Color::from_index(1).unwrap(), - ..Default::default() - }), - (range.end.cast(), format::Colors::default()), - ]; - let r0 = if range.start > 0 { 0 } else { 1 }; - let palette = [col, sel_col]; - self.draw - .text_effects(pos, bb, text, self.cols, &palette, &tokens[r0..]); - } - fn text_cursor(&mut self, id: &Id, pos: Coord, rect: Rect, text: &TextDisplay, byte: usize) { if self.ev.window_has_focus() && !self.w.anim.text_cursor(self.draw.draw, id, byte) { return; diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index 3517581c1..b59e90fba 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -104,17 +104,6 @@ impl Methods { (#base).decorate_text(id, pos, rect, text, palette, decorations); } - fn text_selected_range( - &mut self, - id: &Id, - pos: Coord, - rect: Rect, - text: &TextDisplay, - range: Range, - ) { - (#base).text_selected_range(id, pos, rect, text, range); - } - fn text_cursor( &mut self, id: &Id, diff --git a/crates/kas-wgpu/src/shaded_theme.rs b/crates/kas-wgpu/src/shaded_theme.rs index b93618ad6..6a6e6fb08 100644 --- a/crates/kas-wgpu/src/shaded_theme.rs +++ b/crates/kas-wgpu/src/shaded_theme.rs @@ -7,7 +7,6 @@ use std::cell::RefCell; use std::f32; -use std::ops::Range; use std::time::Instant; use crate::{DrawShaded, DrawShadedImpl}; diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 0fcce9f5f..d136933b0 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -161,9 +161,24 @@ 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 { + return; + }; + let pos = self.rect().pos - offset; + let range: Range = self.selection.range().cast(); - draw.text_with_selection(pos, rect, &self.text, self.selection.range()); + let mut tokens = [(0, format::Colors::default()); 3]; + let tokens = if range.is_empty() { + &[] + } else { + tokens[1].0 = range.start; + tokens[1].1.background = Some(format::Color::default()); + tokens[2].0 = range.end; + let r0 = if range.start > 0 { 0 } else { 1 }; + &tokens[r0..] + }; + draw.text_with_effects(pos, rect, display, &[], tokens); if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { let tokens = [ @@ -175,9 +190,7 @@ impl Component { (edit_range.end, Default::default()), ]; let r0 = if edit_range.start > 0 { 0 } else { 1 }; - if let Ok(display) = self.text.display() { - draw.decorate_text(pos, rect, display, &[], &tokens[r0..]); - } + draw.decorate_text(pos, rect, display, &[], &tokens[r0..]); } if self.editable && draw.ev_state().has_input_focus(self.id_ref()) == Some(true) { diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index 27fb014ea..44376dece 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -10,7 +10,7 @@ use kas::event::components::{ScrollComponent, TextInput, TextInputAction}; use kas::event::{CursorIcon, FocusSource, Scroll}; use kas::prelude::*; use kas::text::SelectionHelper; -use kas::text::format::FormattableText; +use kas::text::format::{self, FormattableText}; use kas::theme::{Text, TextClass}; #[impl_self] @@ -66,13 +66,26 @@ mod SelectableText { } fn draw_with_offset(&self, mut draw: DrawCx, rect: Rect, offset: Offset) { + let Ok(display) = self.text.display() else { + return; + }; + let pos = self.rect().pos - offset; + let range: std::ops::Range = self.selection.range().cast(); - if self.selection.is_empty() { - draw.text_with_position(pos, rect, &self.text); + let mut tokens = [(0, format::Colors::default()); 3]; + let tokens = if range.is_empty() { + &[] } else { - draw.text_with_selection(pos, rect, &self.text, self.selection.range()); - } + tokens[1].0 = range.start; + tokens[1].1.background = Some(format::Color::default()); + tokens[2].0 = range.end; + let r0 = if range.start > 0 { 0 } else { 1 }; + &tokens[r0..] + }; + draw.text_with_effects(pos, rect, display, &[], tokens); + + draw.decorate_text(pos, rect, display, &[], self.text.decorations()); } } From 796f1b547a4c48219801dcbae4c03f0f6317b587 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 20 Feb 2026 09:44:36 +0000 Subject: [PATCH 12/14] Fix text_with_color methods --- crates/kas-core/src/draw/draw.rs | 6 +++++- crates/kas-core/src/theme/draw.rs | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index 8a9140cfb..3ca3f39fc 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -136,7 +136,11 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> { theme: &ColorsLinear, col: Rgba, ) { - self.text_effects(pos, bounding_box, text, theme, &[col], &[]); + let tokens = [(0, format::Colors { + color: format::Color::from_index(0).unwrap(), + ..Default::default() + })]; + self.text_effects(pos, bounding_box, text, theme, &[col], &tokens); } } diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index d3b885698..f2c33d24a 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -307,7 +307,8 @@ impl<'a> DrawCx<'a> { /// Draw text with specified color /// - /// Text is clipped to `rect` and drawn using `color`. + /// Text is clipped to `rect` and drawn using `color`, ignoring any color + /// information from [`Text::color_tokens`]. /// /// This is a convenience method over [`Self::text_with_effects`]. /// @@ -315,8 +316,11 @@ impl<'a> DrawCx<'a> { pub fn text_with_color(&mut self, rect: Rect, text: &Text, color: Rgba) { if let Ok(display) = text.display() { let colors = &[color]; - let tokens = text.color_tokens(); - self.text_with_effects(rect.pos, rect, display, colors, tokens); + let tokens = [(0, format::Colors { + color: format::Color::from_index(0).unwrap(), + ..Default::default() + })]; + self.text_with_effects(rect.pos, rect, display, colors, &tokens); self.decorate_text(rect.pos, rect, display, colors, text.decorations()); } } From 9a85ef42550a350663e7486ff915c0f7d2743ed8 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 20 Feb 2026 10:14:43 +0000 Subject: [PATCH 13/14] Rename text drawing methods --- crates/kas-core/src/draw/draw.rs | 10 ++--- crates/kas-core/src/draw/draw_shared.rs | 2 +- crates/kas-core/src/text/raster.rs | 8 ++-- crates/kas-core/src/theme/draw.rs | 54 ++++++++++++----------- crates/kas-core/src/theme/simple_theme.rs | 4 +- crates/kas-macros/src/extends.rs | 4 +- crates/kas-soft/src/draw.rs | 4 +- crates/kas-wgpu/src/draw/draw_pipe.rs | 4 +- crates/kas-widgets/src/edit/editor.rs | 2 +- crates/kas-widgets/src/scroll_label.rs | 2 +- 10 files changed, 48 insertions(+), 46 deletions(-) diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs index 3ca3f39fc..bec5c57ef 100644 --- a/crates/kas-core/src/draw/draw.rs +++ b/crates/kas-core/src/draw/draw.rs @@ -128,7 +128,7 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> { /// /// The `text` display must be prepared prior to calling this method. /// Typically this is done using a [`crate::theme::Text`] object. - pub fn text( + pub fn text_with_color( &mut self, pos: Vec2, bounding_box: Quad, @@ -140,7 +140,7 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> { color: format::Color::from_index(0).unwrap(), ..Default::default() })]; - self.text_effects(pos, bounding_box, text, theme, &[col], &tokens); + self.text(pos, bounding_box, text, theme, &[col], &tokens); } } @@ -241,7 +241,7 @@ pub trait Draw { /// /// The `text` display must be prepared prior to calling this method. /// Typically this is done using a [`crate::theme::Text`] object. - fn text_effects( + fn text( &mut self, pos: Vec2, bounding_box: Quad, @@ -314,7 +314,7 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { self.shared.draw.draw_image(self.draw, self.pass, id, rect); } - fn text_effects( + fn text( &mut self, pos: Vec2, bb: Quad, @@ -325,7 +325,7 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> { ) { self.shared .draw - .draw_text_effects(self.draw, self.pass, pos, bb, text, theme, palette, tokens); + .draw_text(self.draw, self.pass, pos, bb, text, theme, palette, tokens); } fn decorate_text( diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs index b7ce36ce7..375c6a97c 100644 --- a/crates/kas-core/src/draw/draw_shared.rs +++ b/crates/kas-core/src/draw/draw_shared.rs @@ -208,7 +208,7 @@ pub trait DrawSharedImpl: Any { /// /// The `text` display must be prepared prior to calling this method. /// Typically this is done using a [`crate::theme::Text`] object. - fn draw_text_effects( + fn draw_text( &mut self, draw: &mut Self::Draw, pass: PassId, diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index 93ea11ff7..61635db5d 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -502,8 +502,8 @@ impl State { } } - /// Draw text without effects - pub fn text( + /// Draw text with a single color + pub fn text_with_color( &mut self, allocator: &mut dyn SpriteAllocator, queue: &mut dyn RenderQueue, @@ -535,7 +535,7 @@ impl State { /// Draw text with color effects #[allow(clippy::too_many_arguments)] - pub fn text_effects( + pub fn text( &mut self, allocator: &mut dyn SpriteAllocator, queue: &mut dyn RenderQueue, @@ -556,7 +556,7 @@ impl State { .unwrap_or(true) { let col = Color::default().resolve_color(theme, palette, None); - self.text(allocator, queue, pass, pos, bb, text, col); + self.text_with_color(allocator, queue, pass, pos, bb, text, col); return; } diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index f2c33d24a..990495518 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -275,9 +275,11 @@ impl<'a> DrawCx<'a> { /// Draw text /// - /// Text is clipped to `rect`. + /// Text colors are inferred from [`Text::color_tokens`] and decorations + /// from [`Text::decorations`]. /// - /// This is a convenience method over [`Self::text_with_position`]. + /// 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) { @@ -286,11 +288,12 @@ impl<'a> DrawCx<'a> { /// 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. /// - /// This is a convenience method over [`Self::text_with_effects`]. - /// /// The `text` should be prepared before calling this method. pub fn text_with_position( &mut self, @@ -300,17 +303,18 @@ impl<'a> DrawCx<'a> { ) { if let Ok(display) = text.display() { let tokens = text.color_tokens(); - self.text_with_effects(pos, rect, display, &[], tokens); + self.text_with_colors(pos, rect, display, &[], tokens); self.decorate_text(pos, rect, display, &[], text.decorations()); } } /// Draw text with specified color /// - /// Text is clipped to `rect` and drawn using `color`, ignoring any color - /// information from [`Text::color_tokens`]. + /// The given `color` is used, ignoring [`Text::color_tokens`] + /// Decorations are inferred from [`Text::decorations`]. /// - /// This is a convenience method over [`Self::text_with_effects`]. + /// 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) { @@ -320,24 +324,26 @@ impl<'a> DrawCx<'a> { color: format::Color::from_index(0).unwrap(), ..Default::default() })]; - self.text_with_effects(rect.pos, rect, display, colors, &tokens); + self.text_with_colors(rect.pos, rect, display, colors, &tokens); self.decorate_text(rect.pos, rect, display, colors, text.decorations()); } } - /// Draw text with a list of color effects + /// Draw text with a list of color tokens /// - /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if - /// the text is not scrolled. + /// 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. /// - /// If `palette` is empty, it is replaced with a single theme-defined color. - /// Text is then drawn using `palette[0]` except as specified by effects. + /// This method does not draw decorations; see also [`Self::decorate_text`]. /// - /// The list of `tokens` may be the result of [`Text::color_tokens`] or any - /// compatible sequence (including `&[]`). See also - /// [`FormattableText::color_tokens`]. + /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if + /// the text is not scrolled. #[inline] - pub fn text_with_effects( + pub fn text_with_colors( &mut self, pos: Coord, rect: Rect, @@ -355,14 +361,13 @@ impl<'a> DrawCx<'a> { } } - self.h - .text_effects(&self.id, pos, rect, display, palette, tokens); + self.h.text(&self.id, pos, rect, display, palette, tokens); } /// Draw text decorations (e.g. underlines) /// /// This does not draw the text itself, but requires most of the same inputs - /// as [`Self::text_with_effects`]. + /// as [`Self::text_with_colors`]. /// /// The list of `decorations` may come from [`Text::decorations`] or be any /// other compatible sequence. See also [`FormattableText::decorations`]. @@ -556,16 +561,13 @@ pub trait ThemeDraw { /// Text is clipped to `rect`, drawing from `pos`; use `pos = rect.pos` if /// the text is not scrolled. /// - /// If `palette` is empty, it is replaced with a single theme-defined color. - /// Text is then drawn using `palette[0]` except as specified by effects. - /// /// *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_effects( + fn text( &mut self, id: &Id, pos: Coord, @@ -578,7 +580,7 @@ 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_effects`]. + /// as [`Self::text`]. /// /// The list of `decorations` may come from [`Text::decorations`] or be any /// other compatible sequence. See also [`FormattableText::decorations`]. diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 7d250adf6..c4aced3bb 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -346,7 +346,7 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { } } - fn text_effects( + fn text( &mut self, id: &Id, pos: Coord, @@ -361,7 +361,7 @@ impl<'a, DS: DrawSharedImpl> ThemeDraw for DrawHandle<'a, DS> { let bb = Quad::conv(rect); self.draw - .text_effects(pos.cast(), bb, text, self.cols, palette, tokens); + .text(pos.cast(), bb, text, self.cols, palette, tokens); } fn decorate_text( diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index b59e90fba..5f0918b3e 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -80,7 +80,7 @@ impl Methods { (#base).selection(rect, style); } - fn text_effects( + fn text( &mut self, id: &Id, pos: Coord, @@ -89,7 +89,7 @@ impl Methods { palette: &[Rgba], tokens: &[(u32, ::kas::text::format::Colors)], ) { - (#base).text_effects(id, pos, rect, text, palette, tokens); + (#base).text(id, pos, rect, text, palette, tokens); } fn decorate_text( diff --git a/crates/kas-soft/src/draw.rs b/crates/kas-soft/src/draw.rs index cb6368220..dae34960a 100644 --- a/crates/kas-soft/src/draw.rs +++ b/crates/kas-soft/src/draw.rs @@ -179,7 +179,7 @@ impl DrawSharedImpl for Shared { } } - fn draw_text_effects( + fn draw_text( &mut self, draw: &mut Self::Draw, pass: PassId, @@ -191,7 +191,7 @@ impl DrawSharedImpl for Shared { tokens: &[(u32, text::format::Colors)], ) { let time = std::time::Instant::now(); - self.text.text_effects( + self.text.text( &mut self.images, &mut draw.images, pass, diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index 1865450ae..3cb63eaee 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -357,7 +357,7 @@ impl DrawSharedImpl for DrawPipe { }; } - fn draw_text_effects( + fn draw_text( &mut self, draw: &mut Self::Draw, pass: PassId, @@ -369,7 +369,7 @@ impl DrawSharedImpl for DrawPipe { tokens: &[(u32, text::format::Colors)], ) { let time = std::time::Instant::now(); - self.text.text_effects( + self.text.text( &mut self.images, &mut draw.images, pass, diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index d136933b0..3b6cfd5dc 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -178,7 +178,7 @@ impl Component { let r0 = if range.start > 0 { 0 } else { 1 }; &tokens[r0..] }; - draw.text_with_effects(pos, rect, display, &[], tokens); + draw.text_with_colors(pos, rect, display, &[], tokens); if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { let tokens = [ diff --git a/crates/kas-widgets/src/scroll_label.rs b/crates/kas-widgets/src/scroll_label.rs index 44376dece..ed52501fc 100644 --- a/crates/kas-widgets/src/scroll_label.rs +++ b/crates/kas-widgets/src/scroll_label.rs @@ -83,7 +83,7 @@ mod SelectableText { let r0 = if range.start > 0 { 0 } else { 1 }; &tokens[r0..] }; - draw.text_with_effects(pos, rect, display, &[], tokens); + draw.text_with_colors(pos, rect, display, &[], tokens); draw.decorate_text(pos, rect, display, &[], self.text.decorations()); } From af96065ee68a80561a1850f8ff5de293b23d66d3 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 20 Feb 2026 10:33:49 +0000 Subject: [PATCH 14/14] Clippy --- crates/kas-core/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/kas-core/Cargo.toml b/crates/kas-core/Cargo.toml index a2977930b..32a2a58b2 100644 --- a/crates/kas-core/Cargo.toml +++ b/crates/kas-core/Cargo.toml @@ -143,6 +143,7 @@ neg_cmp_op_on_partial_ord = "allow" collapsible_if = "allow" collapsible_else_if = "allow" bool_comparison = "allow" +option_map_unit_fn = "allow" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(internal_doc)'] }