From f57fd731f171efd3962835e519869d8c05a37521 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sun, 22 Mar 2026 13:14:22 +0000 Subject: [PATCH 1/4] Fix selected text foreground color --- crates/kas-core/src/text/format.rs | 23 +++++++++++++++++------ crates/kas-core/src/text/raster.rs | 6 +++--- crates/kas-widgets/src/edit/highlight.rs | 3 +++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/kas-core/src/text/format.rs b/crates/kas-core/src/text/format.rs index 5efa5bb1e..52ac2fb8f 100644 --- a/crates/kas-core/src/text/format.rs +++ b/crates/kas-core/src/text/format.rs @@ -21,14 +21,13 @@ pub use kas_text::format::FontToken; pub struct Color(NonZeroU32); impl Default for Color { - /// Use a theme-defined color (automatic) fn default() -> Self { Self::DEFAULT } } impl Color { - /// Use the default theme-defined color + /// Use the theme-defined text color /// /// As a foreground color, this maps to [`ColorsLinear::text`] or /// [`ColorsLinear::text_invert`] depending on the background. @@ -37,7 +36,7 @@ impl Color { pub const DEFAULT: Self = Color(NonZeroU32::new(u32::from_ne_bytes(Rgba8Srgb::rgba(1, 0, 0, 0).0)).unwrap()); - /// Use the text-selection color + /// Use the theme-defined text selection color /// /// As a foreground color this is identical to [`Self::DEFAULT`]. /// @@ -82,11 +81,9 @@ impl Color { /// Resolve as (foreground) text color #[inline] - pub fn resolve_foreground(self, theme: &ColorsLinear, bg: Option) -> Rgba { + pub fn resolve_foreground(self, theme: &ColorsLinear) -> Rgba { if let Some(col) = self.as_rgba() { col - } else if let Some(bg) = bg { - theme.text_over(bg) } else { theme.text } @@ -117,6 +114,20 @@ pub struct Colors { pub background: Option, } +impl Colors { + /// Resolve as (foreground) text color + #[inline] + pub fn resolve_foreground(self, theme: &ColorsLinear) -> Rgba { + if let Some(col) = self.foreground.as_rgba() { + col + } else if let Some(bg) = self.background { + theme.text_over(bg.resolve_background(theme)) + } else { + theme.text + } + } +} + /// Decoration types #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/kas-core/src/text/raster.rs b/crates/kas-core/src/text/raster.rs index ec8c3a9d8..fdc324a6c 100644 --- a/crates/kas-core/src/text/raster.rs +++ b/crates/kas-core/src/text/raster.rs @@ -554,7 +554,7 @@ impl State { .map(|e| e.1 == Default::default()) .unwrap_or(true) { - let col = Color::default().resolve_foreground(theme, None); + let col = Color::default().resolve_foreground(theme); self.text_with_color(allocator, queue, pass, pos, bb, text, col); return; } @@ -575,7 +575,7 @@ impl State { } }; - let col = token.foreground.resolve_foreground(theme, None); + let col = token.resolve_foreground(theme); queue.push_sprite(pass, glyph.position.into(), bb, col, sprite); }; @@ -634,7 +634,7 @@ impl State { }; // Known limitation: this cannot depend on the background color. - let col = token.color.resolve_foreground(theme, None); + let col = token.color.resolve_foreground(theme); match token.style { LineStyle::Solid => draw_quad(quad, col), diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index b8ab01c45..c79b010a6 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -47,6 +47,9 @@ pub struct SchemeColors { #[non_exhaustive] pub struct Token { /// Text (foreground) and background color + /// + /// The background color should be `None` unless highlighting is desired. + /// Specify the default background color using [`SchemeColors::background`]. pub colors: Colors, /// Text weight (bold/medium/light) pub weight: FontWeight, From 6fd42375dc41037fd896934b4a7a39734234799b Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sun, 22 Mar 2026 12:55:14 +0000 Subject: [PATCH 2/4] Change default values of SchemeColors' selection fields --- crates/kas-widgets/src/edit/editor.rs | 7 ---- crates/kas-widgets/src/edit/highlight.rs | 37 +++++++++++-------- .../kas-widgets/src/edit/highlight/syntect.rs | 4 +- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 5089789d1..68b0c8bf1 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -15,7 +15,6 @@ use kas::event::{ use kas::geom::{Rect, Vec2}; use kas::layout::{AlignHints, AxisInfo, SizeRules}; use kas::prelude::*; -use kas::text::format::Color; use kas::text::{ConfiguredDisplay, CursorRange, NotReady, SelectionHelper, Status, format}; use kas::theme::{Background, DrawCx, SizeCx, TextClass}; use kas::util::UndoStack; @@ -203,12 +202,6 @@ impl Component { self.0.display.set_max_status(Status::New); } self.0.colors = self.1.scheme_colors(); - if self.0.colors.selection_foreground == Color::default() { - self.0.colors.selection_foreground = Color::SELECTION; - } - if self.0.colors.selection_background == Color::default() { - self.0.colors.selection_background = Color::SELECTION; - } self.0.display.configure(&mut cx.size_cx()); self.prepare(cx); diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index c79b010a6..7bc8b5cb6 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -8,6 +8,7 @@ #[cfg(feature = "syntect")] mod syntect; mod text; +use kas::impl_scope; #[cfg(feature = "syntect")] pub use syntect::{ SyntaxReference as SyntectSyntax, SyntaxSet as SyntectSyntaxSet, SyntectHighlighter, @@ -18,22 +19,26 @@ use kas::event::ConfigCx; use kas::text::fonts::{FontStyle, FontWeight}; use kas::text::format::{Color, Colors, Decoration}; -/// Colors provided by the highlighter's color scheme -/// -/// Note that in each case [`Color::default()`] will resolve to the UI color -/// scheme's color for this property. -#[derive(Debug, Default)] -pub struct SchemeColors { - /// The default text color - pub foreground: Color, - /// The default background color - pub background: Color, - /// The color of the text cursor (sometimes called caret) - pub cursor: Color, - /// The color of selected text - pub selection_foreground: Color, - /// The background color of selected text - pub selection_background: Color, +impl_scope! { + /// Colors provided by the highlighter's color scheme + #[impl_default] + #[derive(Debug)] + pub struct SchemeColors { + /// The default text color + pub foreground: Color, + /// The default background color + pub background: Color, + /// The color of the text cursor (sometimes called caret) + pub cursor: Color, + /// The color of selected text + /// + /// Note that the default value is [`Color::SELECTION`]. + pub selection_foreground: Color = Color::SELECTION, + /// The background color of selected text + /// + /// Note that the default value is [`Color::SELECTION`]. + pub selection_background: Color = Color::SELECTION, + } } /// A highlighting token diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index 6b3f5ce59..926a707ed 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -139,13 +139,13 @@ impl super::Highlighter for SyntectHighlighter { .settings .selection_foreground .and_then(|c| into_kas_text_color(c)) - .unwrap_or_default(), + .unwrap_or(Color::SELECTION), selection_background: self .theme .settings .selection .and_then(|c| into_kas_text_color(c)) - .unwrap_or_default(), + .unwrap_or(Color::SELECTION), } } From 55b19195f5d28a143fc17c3bb093a876e00a28ce Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 23 Mar 2026 10:24:31 +0000 Subject: [PATCH 3/4] Simplify syntect color mapping --- .../kas-widgets/src/edit/highlight/syntect.rs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/kas-widgets/src/edit/highlight/syntect.rs b/crates/kas-widgets/src/edit/highlight/syntect.rs index 926a707ed..fd1d9a5e9 100644 --- a/crates/kas-widgets/src/edit/highlight/syntect.rs +++ b/crates/kas-widgets/src/edit/highlight/syntect.rs @@ -117,13 +117,13 @@ impl super::Highlighter for SyntectHighlighter { .theme .settings .foreground - .and_then(|c| into_kas_text_color(c)) + .map(|c| into_kas_text_color(c)) .unwrap_or_default(), background: self .theme .settings .background - .and_then(|mut c| { + .map(|mut c| { c.a = 255; into_kas_text_color(c) }) @@ -132,19 +132,19 @@ impl super::Highlighter for SyntectHighlighter { .theme .settings .caret - .and_then(|c| into_kas_text_color(c)) + .map(|c| into_kas_text_color(c)) .unwrap_or_default(), selection_foreground: self .theme .settings .selection_foreground - .and_then(|c| into_kas_text_color(c)) + .map(|c| into_kas_text_color(c)) .unwrap_or(Color::SELECTION), selection_background: self .theme .settings .selection - .and_then(|c| into_kas_text_color(c)) + .map(|c| into_kas_text_color(c)) .unwrap_or(Color::SELECTION), } } @@ -168,9 +168,12 @@ impl super::Highlighter for SyntectHighlighter { for (style, _, range) in line_highlighter { let mut token = Token::default(); - token.colors.foreground = - into_kas_text_color(style.foreground).unwrap_or(Default::default()); - token.colors.background = into_kas_text_color(style.background); + token.colors.foreground = into_kas_text_color(style.foreground); + token.colors.background = if style.background.a == 0 { + None + } else { + Some(into_kas_text_color(style.background)) + }; if style.font_style.contains(FontStyle::BOLD) { token.weight = FontWeight::BOLD; } @@ -188,10 +191,7 @@ impl super::Highlighter for SyntectHighlighter { } } -fn into_kas_text_color(c: ::syntect::highlighting::Color) -> Option { - if c.a == 0 { - return None; - } - - Some(Color::from_rgba_srgb(Rgba8Srgb::rgba(c.r, c.g, c.b, c.a))) +/// Convert to `Color`, even if transparent +fn into_kas_text_color(c: ::syntect::highlighting::Color) -> Color { + Color::from_rgba_srgb(Rgba8Srgb::rgba(c.r, c.g, c.b, c.a)) } From 904f0ba81c1f0a137689b02c904552b40dbce6e1 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 20 Mar 2026 10:28:25 +0000 Subject: [PATCH 4/4] Replace highlight::Text with Cache (removes highlighter) --- crates/kas-widgets/src/edit/editor.rs | 34 ++++++------- crates/kas-widgets/src/edit/highlight.rs | 4 +- .../src/edit/highlight/{text.rs => cache.rs} | 49 ++++--------------- 3 files changed, 26 insertions(+), 61 deletions(-) rename crates/kas-widgets/src/edit/highlight/{text.rs => cache.rs} (71%) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 68b0c8bf1..0123c73aa 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -37,6 +37,7 @@ pub struct Editor { id: Id, read_only: bool, display: ConfiguredDisplay, + highlight: highlight::Cache, text: String, colors: SchemeColors, selection: SelectionHelper, @@ -56,6 +57,7 @@ impl Default for Editor { id: Id::default(), read_only: false, display: ConfiguredDisplay::new(TextClass::Editor, false), + highlight: Default::default(), text: Default::default(), colors: SchemeColors::default(), selection: Default::default(), @@ -99,8 +101,8 @@ impl From for Editor { /// support scrolling of text content. Since this component is not a widget it /// cannot implement [`Viewport`] directly, but it does provide the following /// methods: [`Self::content_size`], [`Self::draw_with_offset`]. -#[autoimpl(Debug where H: trait)] -pub struct Component(pub Editor, highlight::Text); +#[derive(Debug, Default)] +pub struct Component(pub Editor, H); impl Deref for Component { type Target = ConfiguredDisplay; @@ -141,17 +143,10 @@ impl Layout for Component { } } -impl Default for Component { - #[inline] - fn default() -> Self { - Component(Editor::default(), Default::default()) - } -} - -impl From for Component { +impl From for Component { #[inline] fn from(text: S) -> Self { - Component(Editor::from(text), Default::default()) + Component(Editor::from(text), H::default()) } } @@ -159,12 +154,12 @@ impl Component { /// Replace the highlighter #[inline] pub fn with_highlighter(self, highlighter: H2) -> Component

{ - Component(self.0, highlight::Text::new(highlighter)) + Component(self.0, highlighter) } /// Set a new highlighter of the same type pub fn set_highlighter(&mut self, highlighter: H) { - self.1 = highlight::Text::new(highlighter); + self.1 = highlighter; } /// Get the background color @@ -210,11 +205,12 @@ impl Component { #[inline] fn prepare_runs(&mut self) { fn inner(this: &mut Component) { - this.1.highlight(&this.0.text); + this.0.highlight.highlight(&this.0.text, &mut this.1); let (dpem, font) = (this.0.display.font_size(), this.0.display.font()); - this.0 - .display - .prepare_runs(this.0.text.as_str(), this.1.font_tokens(dpem, font)); + this.0.display.prepare_runs( + this.0.text.as_str(), + this.0.highlight.font_tokens(dpem, font), + ); } if self.0.display.status() < Status::LevelRuns { @@ -307,7 +303,7 @@ impl Component { let pos = self.rect().pos - offset; let range: Range = self.0.selection.range().cast(); - let color_tokens = self.1.color_tokens(); + let color_tokens = self.0.highlight.color_tokens(); let default_colors = format::Colors { foreground: self.0.colors.foreground, background: None, @@ -391,7 +387,7 @@ impl Component { }; draw.text(pos, rect, display, tokens); - let decorations = self.1.decorations(); + let decorations = self.0.highlight.decorations(); if !decorations.is_empty() { draw.decorate_text(pos, rect, display, decorations); } diff --git a/crates/kas-widgets/src/edit/highlight.rs b/crates/kas-widgets/src/edit/highlight.rs index 7bc8b5cb6..aeef57861 100644 --- a/crates/kas-widgets/src/edit/highlight.rs +++ b/crates/kas-widgets/src/edit/highlight.rs @@ -5,15 +5,15 @@ //! Supporting elements for syntax highlighting +mod cache; #[cfg(feature = "syntect")] mod syntect; -mod text; +pub(crate) use cache::Cache; use kas::impl_scope; #[cfg(feature = "syntect")] pub use syntect::{ SyntaxReference as SyntectSyntax, SyntaxSet as SyntectSyntaxSet, SyntectHighlighter, }; -pub(crate) use text::Text; use kas::event::ConfigCx; use kas::text::fonts::{FontStyle, FontWeight}; diff --git a/crates/kas-widgets/src/edit/highlight/text.rs b/crates/kas-widgets/src/edit/highlight/cache.rs similarity index 71% rename from crates/kas-widgets/src/edit/highlight/text.rs rename to crates/kas-widgets/src/edit/highlight/cache.rs index 29c2860eb..ef7cc2a6c 100644 --- a/crates/kas-widgets/src/edit/highlight/text.rs +++ b/crates/kas-widgets/src/edit/highlight/cache.rs @@ -18,58 +18,27 @@ struct Fmt { } /// A highlighted text -/// -/// Two `Text` objects compare equal if their formatted text is equal regardless -/// of the embedded highlighter. -#[derive(Clone, Debug)] -#[kas::autoimpl(PartialEq ignore self.highlighter)] -pub(crate) struct Text { - highlighter: H, +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct Cache { fonts: Vec, colors: Vec<(u32, Colors)>, decorations: Vec<(u32, Decoration)>, } -impl Default for Text { - fn default() -> Self { - Self::new(H::default()) - } -} - -impl Text { - /// Construct a new instance +impl Default for Cache { #[inline] - pub fn new(highlighter: H) -> Self { - Text { - highlighter, + fn default() -> Self { + Cache { fonts: vec![Fmt::default()], colors: vec![], decorations: vec![], } } +} - /// Configure the highlighter - /// - /// This is called when the widget is configured. It may be used to set the - /// theme / color scheme. - /// - /// Returns `true` when the highlighter must be re-run. - #[inline] - #[must_use] - pub fn configure(&mut self, cx: &mut ConfigCx) -> bool { - self.highlighter.configure(cx) - } - - /// Get scheme colors - /// - /// This method allows usage of the highlighter's colors by the editor. - #[inline] - pub fn scheme_colors(&self) -> SchemeColors { - self.highlighter.scheme_colors() - } - +impl Cache { /// Highlight the text (from scratch) - pub fn highlight(&mut self, text: &str) { + pub fn highlight(&mut self, text: &str, highlighter: &mut H) { self.fonts.clear(); self.fonts.push(Fmt::default()); self.colors.clear(); @@ -110,7 +79,7 @@ impl Text { state = token; }; - if let Err(err) = self.highlighter.highlight_text(text, &mut push_token) { + if let Err(err) = highlighter.highlight_text(text, &mut push_token) { log::error!("Highlighting failed: {err}"); debug_assert!(false, "Highlighter: {err}"); }