From bc7d64987e7b062497105a5ec1ce8d6f433a9c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 30 Aug 2025 18:15:12 +0200 Subject: [PATCH 1/4] Add `Auto` strategy to `text::Shaping` --- Cargo.toml | 4 +++ core/Cargo.toml | 2 ++ core/src/text.rs | 29 +++++++++++++++++-- examples/layout/src/main.rs | 6 ++-- .../catppuccin_frappé-tiny-skia.sha256 | 2 +- graphics/src/text.rs | 9 +++++- graphics/src/text/cache.rs | 2 +- graphics/src/text/paragraph.rs | 4 +-- 8 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index be4070ac..a3ae52c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,10 @@ advanced = ["iced_core/advanced", "iced_widget/advanced"] fira-sans = ["iced_renderer/fira-sans"] # Auto-detects light/dark mode for the built-in theme auto-detect-theme = ["iced_core/auto-detect-theme"] +# Enables basic text shaping by default +basic-shaping = ["iced_core/basic-shaping"] +# Enables advanced text shaping by default +advanced-shaping = ["iced_core/advanced-shaping"] # Enables strict assertions for debugging purposes at the expense of performance strict-assertions = ["iced_renderer/strict-assertions"] # Redraws on every runtime event, and not only when a widget requests it diff --git a/core/Cargo.toml b/core/Cargo.toml index f57aaa4d..cb7ef3ef 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,8 @@ workspace = true auto-detect-theme = ["dep:dark-light"] advanced = [] crisp = [] +basic-shaping = [] +advanced-shaping = [] [dependencies] bitflags.workspace = true diff --git a/core/src/text.rs b/core/src/text.rs index 0b51f244..e47d9bbd 100644 --- a/core/src/text.rs +++ b/core/src/text.rs @@ -130,8 +130,17 @@ impl From for alignment::Horizontal { } /// The shaping strategy of some text. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Shaping { + /// Auto-detect the best shaping strategy from the text. + /// + /// This strategy will use [`Basic`](Self::Basic) shaping if the + /// text consists of only ASCII characters; otherwise, it will + /// use [`Advanced`](Self::Advanced) shaping. + /// + /// This is the default, if neither the `basic-shaping` nor `advanced-shaping` + /// features are enabled. + Auto, /// No shaping and no font fallback. /// /// This shaping strategy is very cheap, but it will not display complex @@ -140,8 +149,8 @@ pub enum Shaping { /// You should use this strategy when you have complete control of the text /// and the font you are displaying in your application. /// - /// This is the default. - #[default] + /// This will be the default if the `basic-shaping` feature is enabled and + /// the `advanced-shaping` feature is disabled. Basic, /// Advanced text shaping and font fallback. /// @@ -150,9 +159,23 @@ pub enum Shaping { /// may be needed to display all of the glyphs. /// /// Advanced shaping is expensive! You should only enable it when necessary. + /// + /// This will be the default if the `advanced-shaping` feature is enabled. Advanced, } +impl Default for Shaping { + fn default() -> Self { + if cfg!(feature = "advanced-shaping") { + Self::Advanced + } else if cfg!(feature = "basic-shaping") { + Self::Basic + } else { + Self::Auto + } + } +} + /// The wrapping strategy of some text. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum Wrapping { diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index c15a0bfc..f3765d9d 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -94,14 +94,14 @@ impl Layout { let controls = row([ (!self.example.is_first()).then_some( - button(text("← Previous").shaping(text::Shaping::Advanced)) + button(text("← Previous")) .padding([5, 10]) .on_press(Message::Previous) .into(), ), Some(horizontal_space().into()), (!self.example.is_last()).then_some( - button(text("Next →").shaping(text::Shaping::Advanced)) + button(text("Next →")) .padding([5, 10]) .on_press(Message::Next) .into(), @@ -313,7 +313,7 @@ fn quotes<'a>() -> Element<'a, Message> { "This is another reply", ), horizontal_rule(1), - text("A separator ↑").shaping(text::Shaping::Advanced), + text("A separator ↑"), ] .width(Shrink) .spacing(10) diff --git a/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 b/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 index cdc79b78..c7a8077d 100644 --- a/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 +++ b/examples/styling/snapshots/catppuccin_frappé-tiny-skia.sha256 @@ -1 +1 @@ -0650eb2c27c21c5d48e1e00031a52d8471d8a3b4e827ad502c4628914f5c1c13 \ No newline at end of file +129523830df064908cfa911214ba61dadc31d8975425ba3efaae69da9514a3d0 \ No newline at end of file diff --git a/graphics/src/text.rs b/graphics/src/text.rs index e00b3ac7..9f932661 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -323,8 +323,15 @@ fn to_align(alignment: Alignment) -> Option { } /// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy. -pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { +pub fn to_shaping(shaping: Shaping, text: &str) -> cosmic_text::Shaping { match shaping { + Shaping::Auto => { + if text.is_ascii() { + cosmic_text::Shaping::Basic + } else { + cosmic_text::Shaping::Advanced + } + } Shaping::Basic => cosmic_text::Shaping::Basic, Shaping::Advanced => cosmic_text::Shaping::Advanced, } diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 9bb66362..a7fc5ddd 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -55,7 +55,7 @@ impl Cache { font_system, key.content, &text::to_attributes(key.font), - text::to_shaping(key.shaping), + text::to_shaping(key.shaping, key.content), ); let bounds = text::align(&mut buffer, font_system, key.align_x); diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index f2c6f404..ea2b30d2 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -88,7 +88,7 @@ impl core::text::Paragraph for Paragraph { font_system.raw(), text.content, &text::to_attributes(text.font), - text::to_shaping(text.shaping), + text::to_shaping(text.shaping, text.content), ); let min_bounds = @@ -158,7 +158,7 @@ impl core::text::Paragraph for Paragraph { (span.text.as_ref(), attrs.metadata(i)) }), &text::to_attributes(text.font), - text::to_shaping(text.shaping), + cosmic_text::Shaping::Advanced, None, ); From 6df435adba58b7954d10662392412c1e986e0ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 30 Aug 2025 18:30:23 +0200 Subject: [PATCH 2/4] Use `Default` implementation of `text::Shaping` --- graphics/src/geometry/text.rs | 2 +- widget/src/overlay/menu.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index a1c3d7fa..867ae4fc 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -176,7 +176,7 @@ impl Default for Text { font: Font::default(), align_x: Alignment::Default, align_y: alignment::Vertical::Top, - shaping: Shaping::Basic, + shaping: Shaping::default(), } } } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index c4c5b521..6f8cc00b 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -73,7 +73,7 @@ where padding: Padding::ZERO, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), font: None, class, } From 665c89a062398f8d9ceff734471746f22e1a4f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 30 Aug 2025 18:34:38 +0200 Subject: [PATCH 3/4] Use `Shaping::Basic` for `icon` helpers in examples --- examples/editor/src/main.rs | 5 ++++- examples/todos/src/main.rs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index ad2337d8..e6243717 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -326,5 +326,8 @@ fn open_icon<'a, Message>() -> Element<'a, Message> { fn icon<'a, Message>(codepoint: char) -> Element<'a, Message> { const ICON_FONT: Font = Font::with_name("editor-icons"); - text(codepoint).font(ICON_FONT).into() + text(codepoint) + .font(ICON_FONT) + .shaping(text::Shaping::Basic) + .into() } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index 65f4a56c..bd1d9379 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -460,6 +460,7 @@ fn icon(unicode: char) -> Text<'static> { .font(Font::with_name("Iced-Todos-Icons")) .width(20) .align_x(Center) + .shaping(text::Shaping::Basic) } fn edit_icon() -> Text<'static> { From e324b18dff6b59d5ff286c0acbe4135e6e8831c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Sat, 30 Aug 2025 18:42:11 +0200 Subject: [PATCH 4/4] Avoid snapshot testing in CI with `--all-features` --- .github/workflows/test.yml | 1 - examples/todos/src/main.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d24f4503..e1280769 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,4 +26,3 @@ jobs: cargo test --verbose --workspace cargo test --verbose --workspace -- --ignored cargo test --verbose --workspace --all-features - cargo test --verbose --workspace --all-features -- --ignored diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index bd1d9379..ed767e70 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -595,6 +595,7 @@ mod tests { } #[test] + #[ignore] fn it_creates_a_new_task() -> Result<(), Error> { let (mut todos, _command) = Todos::new(); let _command = todos.update(Message::Loaded(Err(LoadError::File)));