diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 726cb32bc..408e7b66e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,6 +258,7 @@ jobs: - name: compile to wasm run: cargo build --workspace --target wasm32-unknown-unknown --exclude ribir_dev_helper wasm-test: + needs: lint name: wasm test runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index fe836492e..74da7d4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,11 +25,19 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ## [@Unreleased] - @ReleaseDate +### Features + +- **core**: Added the built-in widget `TextStyleWidgetWidget`, allowing any widget to easily configure the text style within it using `text_style`. (#635, @M-Adoo) + +### Breaking + +- **text**: Removed the `ribir_text` crate and integrated it into the `ribir_painter` crate. (#635 @M-Adoo) + ## [0.4.0-alpha.11] - 2024-10-02 ### Features -- **core**: Added the `PaintingStyle` built-in widget, enabling any widget to utilize `painting_style` to specify how shapes and paths should be painted within its descendants. (#633 @M-Adoo) +- **core**: Added the `PaintingStyleWidget` built-in widget, enabling any widget to utilize `painting_style` to specify how shapes and paths should be painted within its descendants. (#633 @M-Adoo) ### Changed @@ -38,7 +46,6 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he ### Breaking - **text**: Enhance the typography APIs by eliminating `FontSize`, `Pixel`, and `Em`, and directly utilize only logical pixels represented by `f32`. (#629 @M-Adoo) -- **text**: Removed the `ribir_text` crate and integrated it into the `ribir_painter` crate. (#pr @M-Adoo) ## [0.4.0-alpha.10] - 2024-09-25 diff --git a/changelog.config.js b/changelog.config.js index 12b6acf8a..d591720c2 100644 --- a/changelog.config.js +++ b/changelog.config.js @@ -26,7 +26,6 @@ module.exports = { "painter", "macros", "gpu", - "text", "algo", "widgets", "ribir", diff --git a/core/src/builtin_widgets.rs b/core/src/builtin_widgets.rs index d30b1eb69..60cc2dd8a 100644 --- a/core/src/builtin_widgets.rs +++ b/core/src/builtin_widgets.rs @@ -67,6 +67,8 @@ mod class; pub use class::*; mod constrained_box; pub use constrained_box::*; +mod text_style; +pub use text_style::*; use crate::prelude::*; @@ -125,6 +127,7 @@ pub struct FatObj { opacity: Option>, class: Option>, painting_style: Option>, + text_style: Option>, keep_alive: Option>, keep_alive_unsubscribe_handle: Option>, } @@ -188,6 +191,7 @@ impl FatObj { global_anchor: self.global_anchor, class: self.class, painting_style: self.painting_style, + text_style: self.text_style, visibility: self.visibility, opacity: self.opacity, keep_alive: self.keep_alive, @@ -203,6 +207,7 @@ impl FatObj { && self.request_focus.is_none() && self.fitted_box.is_none() && self.box_decoration.is_none() + && self.foreground.is_none() && self.padding.is_none() && self.layout_box.is_none() && self.cursor.is_none() @@ -213,6 +218,9 @@ impl FatObj { && self.v_align.is_none() && self.relative_anchor.is_none() && self.global_anchor.is_none() + && self.class.is_none() + && self.painting_style.is_none() + && self.text_style.is_none() && self.visibility.is_none() && self.opacity.is_none() && self.keep_alive.is_none() @@ -405,6 +413,14 @@ impl FatObj { .get_or_insert_with(|| State::value(<_>::default())) } + /// Returns the `State` widget from the FatObj. If it + /// doesn't exist, a new one will be created. + pub fn get_text_style_widget(&mut self) -> &State { + self + .text_style + .get_or_insert_with(|| State::value(<_>::default())) + } + /// Returns the `State` widget from the FatObj. If it doesn't /// exist, a new one will be created. pub fn get_visibility_widget(&mut self) -> &State { @@ -755,6 +771,11 @@ impl FatObj { self.declare_builtin_init(v, Self::get_painting_style_widget, |m, v| m.painting_style = v) } + /// Initializes the text style of this widget. + pub fn text_style(self, v: impl DeclareInto) -> Self { + self.declare_builtin_init(v, Self::get_text_style_widget, |m, v| m.text_style = v) + } + /// Initializes the background of the widget. pub fn background(self, v: impl DeclareInto, M>) -> Self { self.declare_builtin_init(v, Self::get_box_decoration_widget, |m, v| m.background = v) @@ -925,6 +946,7 @@ impl<'a> FatObj> { relative_anchor, global_anchor, painting_style, + text_style, keep_alive ] ); diff --git a/core/src/builtin_widgets/text_style.rs b/core/src/builtin_widgets/text_style.rs new file mode 100644 index 000000000..ac88f360b --- /dev/null +++ b/core/src/builtin_widgets/text_style.rs @@ -0,0 +1,46 @@ +use crate::{prelude::*, wrap_render::WrapRender}; + +/// This widget establishes the text style for painting the text within its +/// descendants. +#[derive(Default)] +pub struct TextStyleWidget { + pub text_style: TextStyle, +} + +impl Declare for TextStyleWidget { + type Builder = FatObj<()>; + #[inline] + fn declarer() -> Self::Builder { FatObj::new(()) } +} + +impl<'c> ComposeChild<'c> for TextStyleWidget { + type Child = Widget<'c>; + + fn compose_child(this: impl StateWriter, child: Self::Child) -> Widget<'c> { + // We need to provide the text style for the children to access. + match this.try_into_value() { + Ok(this) => { + let style = this.text_style.clone(); + WrapRender::combine_child(State::value(this), child).attach_data(Box::new(Queryable(style))) + } + Err(this) => { + let style = this.map_reader(|w| PartData::from_ref(&w.text_style)); + WrapRender::combine_child(this, child).attach_data(Box::new(style)) + } + } + } +} + +impl WrapRender for TextStyleWidget { + fn perform_layout(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut LayoutCtx) -> Size { + host.perform_layout(clamp, ctx) + } + + fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) { + ctx + .painter() + .set_text_style(self.text_style.clone()); + + host.paint(ctx) + } +} diff --git a/core/src/builtin_widgets/theme.rs b/core/src/builtin_widgets/theme.rs index cc4b8a6ee..f5fa51eba 100644 --- a/core/src/builtin_widgets/theme.rs +++ b/core/src/builtin_widgets/theme.rs @@ -156,6 +156,8 @@ impl> Query for ThemeQuerier { Some(&v.palette) } else if TypeId::of::() == type_id { Some(&v.typography_theme) + } else if TypeId::of::() == type_id { + Some(&v.typography_theme.body_medium.text) } else if TypeId::of::() == type_id { Some(&v.classes) } else if TypeId::of::() == type_id { @@ -279,153 +281,153 @@ fn typography_theme( TypographyTheme { display_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 64., font_size: 57., letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, display_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 52., font_size: 45., letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, display_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 44., font_size: 36., letter_space: 0.0, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, headline_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 40., font_size: 32., letter_space: 0.0, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, headline_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 36., font_size: 28., letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, headline_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 32., font_size: 24., letter_space: 0.0, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, title_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 28., font_size: 22., letter_space: 0.0, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, title_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 24., font_size: 16., letter_space: 0.15, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, title_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 20., font_size: 14., letter_space: 0.1, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, label_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 20.0, font_size: 14.0, letter_space: 0.1, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, label_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 16., font_size: 12., letter_space: 0.5, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, label_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 16.0, font_size: 11., letter_space: 0.5, font_face: medium_face, overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, body_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 24.0, font_size: 16.0, letter_space: 0.5, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, body_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 20.0, font_size: 14., letter_space: 0.25, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, body_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 16., font_size: 12., letter_space: 0.4, font_face: regular_face, overflow: Overflow::Clip, - }), + }, decoration, }, } diff --git a/core/src/builtin_widgets/theme/typography_theme.rs b/core/src/builtin_widgets/theme/typography_theme.rs index 1ad57b4bd..ad434fc95 100644 --- a/core/src/builtin_widgets/theme/typography_theme.rs +++ b/core/src/builtin_widgets/theme/typography_theme.rs @@ -24,7 +24,7 @@ pub struct TypographyTheme { #[derive(Clone, Debug, PartialEq)] pub struct TextTheme { - pub text: CowArc, + pub text: ribir_painter::TextStyle, pub decoration: TextDecorationStyle, } diff --git a/core/src/context/build_ctx.rs b/core/src/context/build_ctx.rs index 14b034979..532a5e556 100644 --- a/core/src/context/build_ctx.rs +++ b/core/src/context/build_ctx.rs @@ -16,8 +16,8 @@ pub struct BuildCtx { /// A node ID has already been allocated for the current building node. pub(crate) pre_alloc: Option, pub(crate) tree: NonNull, - // Todo: Since `Theme`, `Palette`, and `TypographyTheme` are frequently queried during the - // building process, we should cache the closest one. + // Todo: Since `Theme`, `Palette`, `TypographyTheme` and `TextStyle` are frequently queried + // during the building process, layout and paint. we should cache the closest one. } /// A handle of `BuildCtx` that you can store it and access the `BuildCtx` later diff --git a/core/src/widget.rs b/core/src/widget.rs index 50a1ed732..0c4b56962 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -340,4 +340,3 @@ impl Widget<'static> + 'static> From for GenWidget #[inline] fn from(f: F) -> Self { Self::new(f) } } - diff --git a/core/src/window.rs b/core/src/window.rs index 2327f6a01..5af238742 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -304,10 +304,14 @@ impl Window { let root = self.tree_mut().init(self, content); let ctx = BuildCtx::create(root, self.tree); let brush = Palette::of(&*ctx).on_surface_variant(); + let text_style = TypographyTheme::of(&*ctx) + .body_medium + .text + .clone(); self .painter .borrow_mut() - .set_default_brush(brush.into(), brush.into()); + .set_init_state(brush.into(), text_style); } #[inline] diff --git a/dev-helper/src/image_test.rs b/dev-helper/src/image_test.rs index 0917b565e..e9e3fa9fd 100644 --- a/dev-helper/src/image_test.rs +++ b/dev-helper/src/image_test.rs @@ -180,18 +180,45 @@ pub fn wgpu_render_commands( ) -> PixelImage { use futures::executor::block_on; use ribir_geom::{DeviceRect, DeviceSize}; - use ribir_gpu::{GPUBackend, GPUBackendImpl, Texture}; + use ribir_gpu::{GPUBackend, GPUBackendImpl, Texture, WgpuImpl}; use ribir_painter::PainterBackend; - let mut gpu_impl = block_on(ribir_gpu::WgpuImpl::headless()); + let draw_img = |backend: &mut GPUBackend| { + let rect = DeviceRect::from_size(DeviceSize::new(viewport.max_x() + 2, viewport.max_y() + 2)); + let mut texture = backend + .get_impl_mut() + .new_texture(rect.size, ColorFormat::Rgba8); + backend.begin_frame(surface); + backend.draw_commands(rect, commands, &Transform::identity(), &mut texture); + let img = texture.copy_as_image(&rect, backend.get_impl_mut()); + backend.end_frame(); + block_on(img).unwrap() + }; + + #[cfg(not(target_family = "wasm"))] + { + use std::sync::Mutex; + + // Let's ensure that the GPU tests run in single threads and reuse the + // `WgpuImpl`. This is to account for the limited resources of the CI machine, + // which may not support multiple tests simultaneously. + static WGPU_IMPL: Mutex> = Mutex::new(None); - let rect = DeviceRect::from_size(DeviceSize::new(viewport.max_x() + 2, viewport.max_y() + 2)); - let mut texture = gpu_impl.new_texture(rect.size, ColorFormat::Rgba8); - let mut backend = GPUBackend::new(gpu_impl); + let mut container = WGPU_IMPL.lock().unwrap(); + let wgpu_impl = container + .take() + .unwrap_or_else(|| block_on(ribir_gpu::WgpuImpl::headless())); + let mut backend = GPUBackend::new(wgpu_impl); + let img = draw_img(&mut backend); + *container = Some(backend.into_impl()); - backend.begin_frame(surface); - backend.draw_commands(rect, commands, &Transform::identity(), &mut texture); - let img = texture.copy_as_image(&rect, backend.get_impl_mut()); - backend.end_frame(); - block_on(img).unwrap() + img + } + + #[cfg(target_family = "wasm")] + { + let wgpu_impl = block_on(ribir_gpu::WgpuImpl::headless()); + let mut backend = GPUBackend::new(wgpu_impl); + draw_img(&mut backend) + } } diff --git a/gpu/src/gpu_backend.rs b/gpu/src/gpu_backend.rs index 1844ab066..2d8f7bdc5 100644 --- a/gpu/src/gpu_backend.rs +++ b/gpu/src/gpu_backend.rs @@ -153,6 +153,9 @@ where #[inline] pub fn get_impl_mut(&mut self) -> &mut Impl { &mut self.gpu_impl } + #[inline] + pub fn into_impl(self) -> Impl { self.gpu_impl } + fn draw_command( &mut self, cmd: &PaintCommand, global_matrix: &Transform, output_tex_size: DeviceSize, output: &mut Impl::Texture, diff --git a/macros/src/declare_derive.rs b/macros/src/declare_derive.rs index 02796b2a9..3351a4279 100644 --- a/macros/src/declare_derive.rs +++ b/macros/src/declare_derive.rs @@ -434,7 +434,7 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result(mut self, v: impl DeclareInto) -> Self { self.fat_obj = self.fat_obj.foreground(v); self @@ -449,9 +449,15 @@ pub(crate) fn declare_derive(input: &mut syn::DeriveInput) -> syn::Result(mut self, v: impl DeclareInto) -> Self { + self.fat_obj = self.fat_obj.text_style(v); + self + } + + #[doc="Initializes the extra space within the widget."] - #vis fn padding(mut self, v: impl DeclareInto) -> Self - { + #vis fn padding(mut self, v: impl DeclareInto) -> Self { self.fat_obj = self.fat_obj.padding(v); self } diff --git a/macros/src/variable_names.rs b/macros/src/variable_names.rs index 853353804..a9c64a3eb 100644 --- a/macros/src/variable_names.rs +++ b/macros/src/variable_names.rs @@ -116,6 +116,8 @@ pub static BUILTIN_INFOS: phf::Map<&'static str, BuiltinMember> = phf_map! { "foreground" => builtin_member! { "Foreground", Field, "foreground"}, // PaintingStyleWidget "painting_style" => builtin_member! { "PaintingStyleWidget", Field, "painting_style" }, + // TextStyleWidget + "text_style" => builtin_member! { "TextStyleWidget", Field, "text_style" }, // Padding "padding" => builtin_member!{"Padding", Field, "padding"}, // LayoutBox diff --git a/painter/src/painter.rs b/painter/src/painter.rs index 5a89fd1ca..ad1006025 100644 --- a/painter/src/painter.rs +++ b/painter/src/painter.rs @@ -6,9 +6,10 @@ use serde::{Deserialize, Serialize}; use crate::{ color::{LinearGradient, RadialGradient}, + font_db::FontDB, path::*, path_builder::PathBuilder, - Brush, Color, PixelImage, Svg, + Brush, Color, Glyph, PixelImage, Svg, TextStyle, VisualGlyphs, }; /// The painter is a two-dimensional grid. The coordinate (0, 0) is at the /// upper-left corner of the canvas. Along the X-axis, values increase towards @@ -142,6 +143,7 @@ struct PainterState { stroke_brush: Brush, fill_brush: Brush, style: PathStyle, + text_style: TextStyle, transform: Transform, opacity: f32, clip_cnt: usize, @@ -151,12 +153,13 @@ struct PainterState { } impl PainterState { - fn new(bounds: Rect, stroke_brush: Brush, fill_brush: Brush) -> PainterState { + fn new(bounds: Rect) -> PainterState { PainterState { bounds, stroke_options: <_>::default(), - stroke_brush, - fill_brush, + stroke_brush: Color::BLACK.into(), + fill_brush: Color::GRAY.into(), + text_style: TextStyle::default(), transform: Transform::identity(), clip_cnt: 0, opacity: 1., @@ -168,7 +171,7 @@ impl PainterState { impl Painter { pub fn new(viewport: Rect) -> Self { assert!(viewport.is_finite(), "viewport must be finite!"); - let init_state = PainterState::new(viewport, Color::BLACK.into(), Color::GRAY.into()); + let init_state = PainterState::new(viewport); Self { state_stack: vec![init_state.clone()], init_state, @@ -178,10 +181,12 @@ impl Painter { } } - /// Set the default brush of the painter, and then reset the painter state. - pub fn set_default_brush(&mut self, stroke_brush: Brush, fill_brush: Brush) { - self.init_state.fill_brush = fill_brush; - self.init_state.stroke_brush = stroke_brush; + /// Change the default brush and text style of the painter, and then reset + /// the painter state. + pub fn set_init_state(&mut self, brush: Brush, text_style: TextStyle) { + self.init_state.fill_brush = brush.clone(); + self.init_state.stroke_brush = brush; + self.init_state.text_style = text_style; self.reset(); } @@ -249,10 +254,6 @@ impl Painter { #[inline] pub fn stroke_brush(&self) -> &Brush { &self.current_state().stroke_brush } - /// Return the brush used to fill shapes. - #[inline] - pub fn fill_brush(&self) -> &Brush { &self.current_state().fill_brush } - /// Set the brush used to stroke paths. #[inline] pub fn set_stroke_brush(&mut self, brush: impl Into) -> &mut Self { @@ -260,6 +261,10 @@ impl Painter { self } + /// Return the brush used to fill shapes. + #[inline] + pub fn fill_brush(&self) -> &Brush { &self.current_state().fill_brush } + /// Set the brush used to fill shapes. #[inline] pub fn set_fill_brush(&mut self, brush: impl Into) -> &mut Self { @@ -267,6 +272,17 @@ impl Painter { self } + /// Return the current text style used by the painter. + #[inline] + pub fn text_style(&self) -> &TextStyle { &self.current_state().text_style } + + /// Set the text style for the painter. + #[inline] + pub fn set_text_style(&mut self, text_style: TextStyle) -> &mut Self { + self.current_state_mut().text_style = text_style; + self + } + /// return the style for drawing the path. pub fn style(&self) -> PathStyle { self.current_state().style } @@ -637,6 +653,75 @@ impl Painter { self } + pub fn draw_glyph(&mut self, g: &Glyph, font_db: &FontDB) -> &mut Self { + let Some(face) = font_db.try_get_face_data(g.face_id) else { return self }; + + let unit = face.units_per_em() as f32; + let scale = self.text_style().font_size / unit; + + let matrix = *self.transform(); + + let bounds = g.bounds(); + if let Some(path) = face.outline_glyph(g.glyph_id) { + self + .translate(bounds.min_x(), bounds.min_y()) + .scale(scale, -scale) + .translate(0., -unit) + .draw_path(path.into()); + } else if let Some(svg) = face.glyph_svg_image(g.glyph_id) { + let grid_scale = face + .vertical_height() + .map(|h| h as f32 / face.units_per_em() as f32) + .unwrap_or(1.) + .max(1.); + let size = svg.size; + let bound_size = bounds.size; + let scale = (bound_size.width / size.width).min(bound_size.height / size.height) / grid_scale; + self + .translate(bounds.min_x(), bounds.min_y()) + .scale(scale, scale) + .draw_svg(&svg); + } else if let Some(img) = + face.glyph_raster_image(g.glyph_id, (unit / self.text_style().font_size) as u16) + { + let m_width = img.width() as f32; + let m_height = img.height() as f32; + let scale = (bounds.width() / m_width).min(bounds.height() / m_height); + + let x_offset = bounds.min_x() + (bounds.width() - (m_width * scale)) / 2.; + let y_offset = bounds.min_y() + (bounds.height() - (m_height * scale)) / 2.; + + self + .translate(x_offset, y_offset) + .scale(scale, scale) + .draw_img(img, &Rect::from_size(Size::new(m_width, m_height)), &None); + } + + self.set_transform(matrix); + + self + } + + /// draw the text glyphs within the box_rect + pub fn draw_glyphs_in_rect( + self: &mut Painter, visual_glyphs: &VisualGlyphs, box_rect: Rect, font_db: &FontDB, + ) -> &mut Self { + let visual_rect = visual_glyphs.visual_rect(); + let Some(paint_rect) = self.intersection_paint_bounds(&box_rect) else { + return self; + }; + if !paint_rect.contains_rect(&visual_rect) { + self.clip(Path::rect(&paint_rect).into()); + } + self.translate(visual_rect.origin.x, visual_rect.origin.y); + + for g in visual_glyphs.glyphs_in_bounds(&paint_rect) { + self.draw_glyph(&g, font_db); + } + + self + } + fn swap_brush(&mut self) { let state = self.current_state_mut(); std::mem::swap(&mut state.fill_brush, &mut state.stroke_brush); diff --git a/painter/src/text.rs b/painter/src/text.rs index 410b5b5b8..7319d009b 100644 --- a/painter/src/text.rs +++ b/painter/src/text.rs @@ -9,18 +9,14 @@ use std::hash::Hash; use derive_more::{Add, AddAssign, Mul, Neg, Sub, SubAssign}; use font_db::Face; pub use fontdb::{Stretch as FontStretch, Style as FontStyle, Weight as FontWeight, ID}; -use ribir_algo::CowArc; pub use ribir_algo::Substr; -use ribir_geom::{Rect, Size}; +use ribir_geom::{rect, Rect}; use rustybuzz::{ttf_parser::GlyphId, GlyphPosition}; -use typography::PlaceLineDirection; pub mod text_reorder; pub mod typography; pub use text_reorder::TextReorder; mod typography_store; pub use typography_store::{TypographyStore, VisualGlyphs}; -mod text_render; -pub use text_render::{draw_glyphs, draw_glyphs_in_rect}; mod svg_glyph_cache; mod text_writer; @@ -37,7 +33,6 @@ pub mod unicode_help; /// A [font family](https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#propdef-font-family). #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum FontFamily { - // todo: no need cow? or directly face ids /// The name of a font family of choice. Name(std::borrow::Cow<'static, str>), @@ -139,18 +134,6 @@ pub struct Glyph { )] pub struct GlyphUnit(i32); -#[derive(Debug, Clone)] -pub struct GlyphBound { - /// The font face id of the glyph. - pub face_id: ID, - /// The pixel bound rect of the glyph. - pub bound: Rect, - /// The id of the glyph. - pub glyph_id: GlyphId, - /// An cluster of origin text as byte index. - pub cluster: u32, -} - impl Default for FontFace { fn default() -> Self { Self { @@ -252,6 +235,15 @@ impl Glyph { self.y_offset = cast(self.y_offset.0, scale); self } + + pub fn bounds(&self) -> Rect { + rect( + self.x_offset.into_pixel(), + self.y_offset.into_pixel(), + self.x_advance.into_pixel(), + self.y_advance.into_pixel(), + ) + } } impl std::ops::Div for GlyphUnit { @@ -261,22 +253,6 @@ impl std::ops::Div for GlyphUnit { fn div(self, rhs: f32) -> Self::Output { cast(self.0, 1. / rhs) } } -pub trait VisualText { - fn text(&self) -> CowArc; - fn text_style(&self) -> &TextStyle; - fn text_align(&self) -> TextAlign; - - fn text_layout(&self, typography_store: &mut TypographyStore, bounds: Size) -> VisualGlyphs { - typography_store.typography( - self.text().substr(..), - self.text_style(), - bounds, - self.text_align(), - PlaceLineDirection::TopToBottom, - ) - } -} - impl Default for TextStyle { fn default() -> Self { Self { diff --git a/painter/src/text/text_render.rs b/painter/src/text/text_render.rs deleted file mode 100644 index 8ddfdebb3..000000000 --- a/painter/src/text/text_render.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::{cell::RefCell, ops::Deref}; - -use ribir_algo::Sc; -use ribir_geom::{Rect, Size}; - -use crate::{font_db::FontDB, GlyphBound, Painter, Path, VisualGlyphs}; - -/// draw the text glyphs within the box_rect, with the given brush font_size and -/// path style -pub fn draw_glyphs_in_rect( - painter: &mut Painter, visual_glyphs: VisualGlyphs, box_rect: Rect, font_size: f32, - font_db: Sc>, -) { - let visual_rect = visual_glyphs.visual_rect(); - let Some(paint_rect) = painter.intersection_paint_bounds(&box_rect) else { - return; - }; - if !paint_rect.contains_rect(&visual_rect) { - painter.clip(Path::rect(&paint_rect).into()); - } - painter.translate(visual_rect.origin.x, visual_rect.origin.y); - draw_glyphs(painter, visual_glyphs.glyph_bounds_in_rect(&paint_rect), font_size, font_db); -} - -/// draw the glyphs with the given brush, font_size and path style -pub fn draw_glyphs( - painter: &mut Painter, glyphs: impl Iterator, font_size: f32, - font_db: Sc>, -) { - glyphs.for_each(|g| { - let font_db = font_db.borrow(); - let face = font_db.try_get_face_data(g.face_id); - - if let Some(face) = face { - let unit = face.units_per_em() as f32; - let scale = font_size / unit; - let mut painter = painter.save_guard(); - if let Some(path) = face.outline_glyph(g.glyph_id) { - painter - .translate(g.bound.min_x(), g.bound.min_y()) - .scale(scale, -scale) - .translate(0., -unit); - match painter.style() { - crate::PathStyle::Fill => painter.fill_path(path.into()), - crate::PathStyle::Stroke => painter.stroke_path(path.deref().clone()), - }; - } else if let Some(svg) = face.glyph_svg_image(g.glyph_id) { - let grid_scale = face - .vertical_height() - .map(|h| h as f32 / face.units_per_em() as f32) - .unwrap_or(1.) - .max(1.); - let size = svg.size; - let bound_size = g.bound.size; - let scale = - (bound_size.width / size.width).min(bound_size.height / size.height) / grid_scale; - painter - .translate(g.bound.min_x(), g.bound.min_y()) - .scale(scale, scale) - .draw_svg(&svg); - } else if let Some(img) = face.glyph_raster_image(g.glyph_id, (unit / font_size) as u16) { - let m_width = img.width() as f32; - let m_height = img.height() as f32; - let scale = (g.bound.width() / m_width).min(g.bound.height() / m_height); - - let x_offset = g.bound.min_x() + (g.bound.width() - (m_width * scale)) / 2.; - let y_offset = g.bound.min_y() + (g.bound.height() - (m_height * scale)) / 2.; - - painter - .translate(x_offset, y_offset) - .scale(scale, scale) - .draw_img(img, &Rect::from_size(Size::new(m_width, m_height)), &None); - } - } - }); -} diff --git a/painter/src/text/typography_store.rs b/painter/src/text/typography_store.rs index b745d9da0..d58ee0149 100644 --- a/painter/src/text/typography_store.rs +++ b/painter/src/text/typography_store.rs @@ -38,6 +38,8 @@ pub struct TypographyStore { font_db: Sc>, cache: FrameCache>, } + +#[derive(Clone)] pub struct VisualGlyphs { font_size: f32, x: GlyphUnit, @@ -402,7 +404,7 @@ impl VisualGlyphs { .map(move |g| g.cast_to(self.font_size)) } - pub fn glyph_bounds_in_rect(&self, rc: &Rect) -> impl Iterator + '_ { + pub fn glyphs_in_bounds(&self, rc: &Rect) -> impl Iterator + '_ { let visual_rect = self.visual_rect(); let mut rc = visual_rect.intersection(rc).unwrap_or_default(); rc.origin -= visual_rect.origin.to_vector(); @@ -432,15 +434,6 @@ impl VisualGlyphs { }) .filter(move |g| !(g.x_offset + g.x_advance < min_x || max_x < g.x_offset)) .map(move |g| g.cast_to(self.font_size)) - .map(|g| GlyphBound { - face_id: g.face_id, - bound: Rect::new( - Point::new(g.x_offset.into_pixel(), g.y_offset.into_pixel()), - ribir_geom::Size::new(g.x_advance.into_pixel(), g.y_advance.into_pixel()), - ), - glyph_id: g.glyph_id, - cluster: g.cluster, - }) } pub fn glyph_count(&self, row: usize, ignore_new_line: bool) -> usize { @@ -586,8 +579,11 @@ mod tests { let info = typography_text(text, &style, bounds, text_align, line_dir); let visual_rc = info.visual_rect(); info - .glyph_bounds_in_rect(&Rect::from_size(bounds)) - .map(|g| (visual_rc.origin.x + g.bound.min_x(), visual_rc.origin.y + g.bound.min_y())) + .glyphs_in_bounds(&Rect::from_size(bounds)) + .map(|g| { + let bounds = g.bounds(); + (visual_rc.origin.x + bounds.min_x(), visual_rc.origin.y + bounds.min_y()) + }) .collect() } diff --git a/ribir/Cargo.toml b/ribir/Cargo.toml index 09f2ea850..45da259fd 100644 --- a/ribir/Cargo.toml +++ b/ribir/Cargo.toml @@ -48,7 +48,7 @@ ribir_dev_helper = { path = "../dev-helper" } ribir_material = { path = "../themes/material" } [features] -default = ["wgpu", "widgets", "material"] +default = ["wgpu", "widgets", "material", "png"] material = ["ribir_material"] png = ["ribir_core/png"] wgpu = ["ribir_gpu/wgpu", "dep:wgpu"] diff --git a/test_cases/ribir_widgets/text/tests/default_text_with_default_by_wgpu.png b/test_cases/ribir_widgets/text/tests/default_text_with_default_by_wgpu.png new file mode 100644 index 000000000..e5619f2f7 Binary files /dev/null and b/test_cases/ribir_widgets/text/tests/default_text_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/text/tests/default_text_with_material_by_wgpu.png b/test_cases/ribir_widgets/text/tests/default_text_with_material_by_wgpu.png new file mode 100644 index 000000000..4693fe6dd Binary files /dev/null and b/test_cases/ribir_widgets/text/tests/default_text_with_material_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/text/tests/h1_with_default_by_wgpu.png b/test_cases/ribir_widgets/text/tests/h1_with_default_by_wgpu.png new file mode 100644 index 000000000..8ccec73a6 Binary files /dev/null and b/test_cases/ribir_widgets/text/tests/h1_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/text/tests/h1_with_material_by_wgpu.png b/test_cases/ribir_widgets/text/tests/h1_with_material_by_wgpu.png new file mode 100644 index 000000000..376907567 Binary files /dev/null and b/test_cases/ribir_widgets/text/tests/h1_with_material_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/text/tests/text_clip_with_default_by_wgpu.png b/test_cases/ribir_widgets/text/tests/text_clip_with_default_by_wgpu.png new file mode 100644 index 000000000..b46b74435 Binary files /dev/null and b/test_cases/ribir_widgets/text/tests/text_clip_with_default_by_wgpu.png differ diff --git a/test_cases/ribir_widgets/text/tests/text_clip_with_material_by_wgpu.png b/test_cases/ribir_widgets/text/tests/text_clip_with_material_by_wgpu.png new file mode 100644 index 000000000..240c72586 Binary files /dev/null and b/test_cases/ribir_widgets/text/tests/text_clip_with_material_by_wgpu.png differ diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 796ecd6e5..bc1d77f8b 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -18,6 +18,7 @@ paste.workspace = true ribir = {path = "../ribir", features = ["material", "widgets"]} ribir_dev_helper = {path = "../dev-helper"} ribir_geom = {path = "../geom"} +ribir_painter = {path = "../painter"} winit.workspace = true criterion = "0.5.1" todos = {path = "../examples/todos"} diff --git a/tests/benches/text_bench.rs b/tests/benches/text_bench.rs index 01146461c..9b9ffe621 100644 --- a/tests/benches/text_bench.rs +++ b/tests/benches/text_bench.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use ribir_text::{shaper::*, *}; +use ribir_painter::{shaper::*, *}; fn shape_1k(c: &mut Criterion) { let mut shaper = TextShaper::new(<_>::default()); diff --git a/text/src/text_render.rs b/text/src/text_render.rs deleted file mode 100644 index 62e853bee..000000000 --- a/text/src/text_render.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{cell::RefCell, ops::Deref}; - -use ribir_algo::Sc; -use ribir_geom::{Rect, Size}; -use ribir_painter::{Painter, Path}; - -use crate::{ - font_db::{Face, FontDB}, - GlyphBound, VisualGlyphs, -}; - -/// draw the text glyphs within the box_rect, with the given brush font_size and -/// path style -pub fn draw_glyphs_in_rect( - painter: &mut Painter, visual_glyphs: VisualGlyphs, box_rect: Rect, font_size: f32, - font_db: Sc>, -) { - let visual_rect = visual_glyphs.visual_rect(); - let Some(paint_rect) = painter.intersection_paint_bounds(&box_rect) else { - return; - }; - if !paint_rect.contains_rect(&visual_rect) { - painter.clip(Path::rect(&paint_rect).into()); - } - painter.translate(visual_rect.origin.x, visual_rect.origin.y); - draw_glyphs(painter, visual_glyphs.glyph_bounds_in_rect(&paint_rect), font_size, font_db); -} - -/// draw the glyphs with the given brush, font_size and path style -pub fn draw_glyphs( - painter: &mut Painter, glyphs: impl Iterator, font_size: f32, - font_db: Sc>, -) { - glyphs.for_each(|g| { - let font_db = font_db.borrow(); - let face = font_db.try_get_face_data(g.face_id); - - if let Some(face) = face { - let unit = face.units_per_em() as f32; - let scale = font_size / unit; - let mut painter = painter.save_guard(); - if let Some(path) = face.outline_glyph(g.glyph_id) { - painter - .translate(g.bound.min_x(), g.bound.min_y()) - .scale(scale, -scale) - .translate(0., -unit); - match painter.style() { - ribir_painter::PathStyle::Fill => painter.fill_path(path.into()), - ribir_painter::PathStyle::Stroke => painter.stroke_path(path.deref().clone()), - }; - } else if let Some(svg) = face.glyph_svg_image(g.glyph_id) { - let grid_scale = face - .vertical_height() - .map(|h| h as f32 / face.units_per_em() as f32) - .unwrap_or(1.) - .max(1.); - let size = svg.size; - let bound_size = g.bound.size; - let scale = - (bound_size.width / size.width).min(bound_size.height / size.height) / grid_scale; - painter - .translate(g.bound.min_x(), g.bound.min_y()) - .scale(scale, scale) - .draw_svg(&svg); - } else if let Some(img) = face.glyph_raster_image(g.glyph_id, (unit / font_size) as u16) { - let m_width = img.width() as f32; - let m_height = img.height() as f32; - let scale = (g.bound.width() / m_width).min(g.bound.height() / m_height); - - let x_offset = g.bound.min_x() + (g.bound.width() - (m_width * scale)) / 2.; - let y_offset = g.bound.min_y() + (g.bound.height() - (m_height * scale)) / 2.; - - painter - .translate(x_offset, y_offset) - .scale(scale, scale) - .draw_img(img, &Rect::from_size(Size::new(m_width, m_height)), &None); - } - } - }); -} diff --git a/themes/material/src/lib.rs b/themes/material/src/lib.rs index 7c9ca8cb6..bcdaf5e60 100644 --- a/themes/material/src/lib.rs +++ b/themes/material/src/lib.rs @@ -414,153 +414,153 @@ pub fn typography_theme( TypographyTheme { display_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 64., font_size: 57., letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, display_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 52.0, font_size: 45.0, letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, display_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 44.0, font_size: 36.0, letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, headline_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 40.0, font_size: 32.0, letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, headline_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 36.0, font_size: 28.0, letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, headline_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 32.0, font_size: 24.0, letter_space: 0., font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, title_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 28.0, font_size: 22.0, letter_space: 0., font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, title_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 24.0, font_size: 16.0, letter_space: 0.15, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, title_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 20.0, font_size: 14.0, letter_space: 0.1, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, label_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 20.0, font_size: 14.0, letter_space: 0.1, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, label_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 16.0, font_size: 12.0, letter_space: 0.5, font_face: medium_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, label_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 16.0, font_size: 11.0, letter_space: 0.5, font_face: medium_face, overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, body_large: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 24.0, font_size: 16.0, letter_space: 0.5, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, body_medium: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 20.0, font_size: 14.0, letter_space: 0.25, font_face: regular_face.clone(), overflow: Overflow::Clip, - }), + }, decoration: decoration.clone(), }, body_small: TextTheme { - text: CowArc::owned(TextStyle { + text: TextStyle { line_height: 16.0, font_size: 12.0, letter_space: 0.4, font_face: regular_face, overflow: Overflow::Clip, - }), + }, decoration, }, } diff --git a/widgets/src/avatar.rs b/widgets/src/avatar.rs index b45a7bdc4..538b2048c 100644 --- a/widgets/src/avatar.rs +++ b/widgets/src/avatar.rs @@ -33,7 +33,7 @@ pub struct Avatar { pub struct AvatarStyle { pub size: Size, pub radius: Option, - pub text_style: CowArc, + pub text_style: TextStyle, } impl CustomStyle for AvatarStyle { diff --git a/widgets/src/buttons.rs b/widgets/src/buttons.rs index ae36edae6..7c76532a6 100644 --- a/widgets/src/buttons.rs +++ b/widgets/src/buttons.rs @@ -11,7 +11,7 @@ pub struct ButtonImpl { pub label_gap: f32, #[allow(unused)] pub icon_pos: IconPosition, - pub label_style: CowArc, + pub label_style: TextStyle, pub foreground_color: Brush, pub background_color: Option, pub radius: Option, diff --git a/widgets/src/buttons/button.rs b/widgets/src/buttons/button.rs index 2e27983bc..f01fe863e 100644 --- a/widgets/src/buttons/button.rs +++ b/widgets/src/buttons/button.rs @@ -8,7 +8,7 @@ pub struct ButtonStyle { pub icon_size: Size, pub label_gap: f32, pub icon_pos: IconPosition, - pub label_style: CowArc, + pub label_style: TextStyle, pub padding_style: EdgeInsets, } diff --git a/widgets/src/buttons/fab_button.rs b/widgets/src/buttons/fab_button.rs index 04f48b723..a9ee0bbb3 100644 --- a/widgets/src/buttons/fab_button.rs +++ b/widgets/src/buttons/fab_button.rs @@ -8,7 +8,7 @@ pub struct FabButtonStyle { pub icon_size: Size, pub label_gap: f32, pub icon_pos: IconPosition, - pub label_style: CowArc, + pub label_style: TextStyle, pub radius: f32, pub padding_style: EdgeInsets, } diff --git a/widgets/src/buttons/filled_button.rs b/widgets/src/buttons/filled_button.rs index de7f62dde..67451d1e0 100644 --- a/widgets/src/buttons/filled_button.rs +++ b/widgets/src/buttons/filled_button.rs @@ -8,7 +8,7 @@ pub struct FilledButtonStyle { pub icon_size: Size, pub label_gap: f32, pub icon_pos: IconPosition, - pub label_style: CowArc, + pub label_style: TextStyle, pub radius: f32, pub padding_style: EdgeInsets, } diff --git a/widgets/src/buttons/outlined_button.rs b/widgets/src/buttons/outlined_button.rs index be82f4fe9..14283cc68 100644 --- a/widgets/src/buttons/outlined_button.rs +++ b/widgets/src/buttons/outlined_button.rs @@ -8,7 +8,7 @@ pub struct OutlinedButtonStyle { pub icon_size: Size, pub label_gap: f32, pub icon_pos: IconPosition, - pub label_style: CowArc, + pub label_style: TextStyle, pub radius: f32, pub padding_style: EdgeInsets, pub border_width: f32, diff --git a/widgets/src/checkbox.rs b/widgets/src/checkbox.rs index 0958ef2aa..be61abf45 100644 --- a/widgets/src/checkbox.rs +++ b/widgets/src/checkbox.rs @@ -21,7 +21,7 @@ pub struct CheckBoxStyle { /// The size of the checkbox icon. pub icon_size: Size, /// The text style of the checkbox label. - pub label_style: CowArc, + pub label_style: TextStyle, /// The checkbox foreground pub label_color: Brush, } diff --git a/widgets/src/input.rs b/widgets/src/input.rs index ee8637407..19bb6322a 100644 --- a/widgets/src/input.rs +++ b/widgets/src/input.rs @@ -33,7 +33,7 @@ impl Placeholder { #[derive(Clone)] pub struct PlaceholderStyle { - pub text_style: CowArc, + pub text_style: TextStyle, pub foreground: Brush, } @@ -80,7 +80,7 @@ pub trait EditableText: Sized { #[derive(Declare)] pub struct Input { #[declare(default = TypographyTheme::of(ctx!()).body_large.text.clone())] - pub style: CowArc, + pub style: TextStyle, #[declare(skip)] text: CowArc, #[declare(skip)] @@ -92,7 +92,7 @@ pub struct Input { #[derive(Declare)] pub struct TextArea { #[declare(default = TypographyTheme::of(ctx!()).body_large.text.clone())] - pub style: CowArc, + pub style: TextStyle, #[declare(default = true)] pub auto_wrap: bool, #[declare(skip)] @@ -336,11 +336,10 @@ where Self: 'static, { fn edit_area( - this: impl StateWriter, mut text: FatObj>, + this: impl StateWriter, text: FatObj>, scroll_dir: impl Pipe, placeholder: Option, ) -> Widget<'static> { fn_widget! { - let layout_box = text.get_layout_box_widget().clone_reader(); let only_text = text.clone_reader(); let mut stack = @Stack { @@ -351,14 +350,14 @@ where let caret_box = @Caret { focused: pipe!($stack.has_focus()), clamp: pipe!( - $this.current_line_height(&$text, $text.layout_size()).unwrap_or(0.) + $this.current_line_height(&$text).unwrap_or(0.) ).map(BoxClamp::fixed_height), }; let caret_box_id = caret_box.lazy_host_id(); let mut caret_box = @$caret_box { anchor: pipe!( - let pos = $this.caret_position(&$text, $text.layout_size()).unwrap_or_default(); + let pos = $this.caret_position(&$text).unwrap_or_default(); Anchor::left_top(pos.x, pos.y) ), }; @@ -369,7 +368,7 @@ where watch!(SelectableText::caret(&*$this)) .distinct_until_changed() .sample(tick_of_layout_ready) - .map(move |_| $this.caret_position(&$text, $text.layout_size()).unwrap_or_default()) + .map(move |_| $this.caret_position(&$text).unwrap_or_default()) .scan_initial((Point::zero(), Point::zero()), |pair, v| (pair.1, v)) .subscribe(move |(before, after)| { let mut scrollable = $scrollable.silent(); @@ -396,7 +395,7 @@ where }, on_key_down: move |k| { let _hint_capture_writer = || $this.write(); - select_key_handle(&this, &$only_text, &$layout_box, k); + select_key_handle(&this, &$only_text, k); edit_key_handle(&this, k); }, on_ime_pre_edit: move |e| { @@ -409,7 +408,7 @@ where @OnlySizedByParent { @SelectedHighLight { visible: pipe!($stack.has_focus()), - rects: pipe! { $this.select_text_rect(&$text, $text.layout_size()) } + rects: pipe! { $this.select_text_rect(&$text) } } } }; @@ -423,8 +422,7 @@ where let text_widget = bind_point_listener( this.clone_writer(), text_widget, - only_text, - layout_box + only_text ); @ $stack { diff --git a/widgets/src/input/text_selectable.rs b/widgets/src/input/text_selectable.rs index ff8d6c4f9..f03390d49 100644 --- a/widgets/src/input/text_selectable.rs +++ b/widgets/src/input/text_selectable.rs @@ -31,24 +31,28 @@ pub trait SelectableText { fn set_caret(&mut self, caret: CaretState); - fn select_text_rect(&self, text: &Text, text_size: Size) -> Vec { - let glyphs = text.text_layout(&mut AppCtx::typography_store().borrow_mut(), text_size); - let helper = TextGlyphsHelper::new(text.text.clone(), glyphs); - helper - .selection(self.text(), &self.select_range()) + fn select_text_rect(&self, text: &Text) -> Vec { + text + .glyphs() + .and_then(|glyphs| { + let helper = TextGlyphsHelper::new(text.text.clone(), glyphs.clone()); + helper.selection(self.text(), &self.select_range()) + }) .unwrap_or_default() } - fn caret_position(&self, text: &Text, text_size: Size) -> Option { - let glyphs = text.text_layout(&mut AppCtx::typography_store().borrow_mut(), text_size); - let helper = TextGlyphsHelper::new(text.text.clone(), glyphs); - helper.cursor(self.text(), self.caret().caret_position()) + fn caret_position(&self, text: &Text) -> Option { + text.glyphs().and_then(|glyphs| { + let helper = TextGlyphsHelper::new(text.text.clone(), glyphs.clone()); + helper.cursor(self.text(), self.caret().caret_position()) + }) } - fn current_line_height(&self, text: &Text, text_size: Size) -> Option { - let glyphs = text.text_layout(&mut AppCtx::typography_store().borrow_mut(), text_size); - let helper = TextGlyphsHelper::new(text.text.clone(), glyphs); - helper.line_height(self.text(), self.caret().caret_position()) + fn current_line_height(&self, text: &Text) -> Option { + text.glyphs().and_then(|glyphs| { + let helper = TextGlyphsHelper::new(text.text.clone(), glyphs.clone()); + helper.line_height(self.text(), self.caret().caret_position()) + }) } } @@ -67,41 +71,37 @@ impl TextSelectable { } pub(crate) fn bind_point_listener( - this: impl StateWriter + 'static, host: Widget, - text: Reader, layout_box: Reader, + this: impl StateWriter + 'static, host: Widget, text: Reader, ) -> Widget { fn_widget! { @$host { on_pointer_down: move |e| { - let _hint_capture_reader = || $layout_box; let mut this = $this.write(); let position = e.position(); - let layout_size = layout_box.read().layout_size(); - let helper = $text.text_layout(&mut AppCtx::typography_store().borrow_mut(), layout_size); - let end = helper.caret_position_from_pos(position.x, position.y); - let begin = if e.with_shift_key() { - match this.caret() { - CaretState::Caret(begin) | - CaretState::Select(begin, _) | - CaretState::Selecting(begin, _) => begin, - } - } else { - end - }; - this.set_caret(CaretState::Selecting(begin, end)); + if let Some(helper) = $text.glyphs() { + let end = helper.caret_position_from_pos(position.x, position.y); + let begin = if e.with_shift_key() { + match this.caret() { + CaretState::Caret(begin) | + CaretState::Select(begin, _) | + CaretState::Selecting(begin, _) => begin, + } + } else { + end + }; + this.set_caret(CaretState::Selecting(begin, end)); + } }, on_pointer_move: move |e| { - let _hint_capture_reader = || $layout_box; let mut this = $this.write(); if let CaretState::Selecting(begin, _) = this.caret() { if e.point_type == PointerType::Mouse && e.mouse_buttons() == MouseButtons::PRIMARY { - let position = e.position(); - let layout_size = layout_box.read().layout_size(); - let store = AppCtx::typography_store(); - let helper = $text.text_layout(&mut store.borrow_mut(), layout_size); - let end = helper.caret_position_from_pos(position.x, position.y); - this.set_caret(CaretState::Selecting(begin, end)); + if let Some(glyphs) = $text.glyphs() { + let position = e.position(); + let end = glyphs.caret_position_from_pos(position.x, position.y); + this.set_caret(CaretState::Selecting(begin, end)); + } } } }, @@ -118,17 +118,15 @@ pub(crate) fn bind_point_listener( } }, on_double_tap: move |e| { - let _hint_capture_reader = || $layout_box; - let position = e.position(); - - let layout_size = layout_box.read().layout_size(); - let helper = $text.text_layout(&mut AppCtx::typography_store().borrow_mut(), layout_size); - let caret = helper.caret_position_from_pos(position.x, position.y); - let rg = select_word(&$text.text(), caret.cluster); - $this.write().set_caret(CaretState::Select( - CaretPosition { cluster: rg.start, position: None }, - CaretPosition { cluster: rg.end, position: None } - )); + if let Some(glyphs) = $text.glyphs() { + let position = e.position(); + let caret = glyphs.caret_position_from_pos(position.x, position.y); + let rg = select_word(&$text.text, caret.cluster); + $this.write().set_caret(CaretState::Select( + CaretPosition { cluster: rg.start, position: None }, + CaretPosition { cluster: rg.end, position: None } + )); + } } } } @@ -141,7 +139,7 @@ impl ComposeChild<'static> for TextSelectable { let src = text.into_inner(); fn_widget! { - let mut text = @ $src {}; + let text = @ $src {}; $this.silent().text = $text.text.clone(); watch!($text.text.clone()) .subscribe(move |v| { @@ -150,7 +148,6 @@ impl ComposeChild<'static> for TextSelectable { } }); - let layout_box = text.get_layout_box_widget().clone_reader(); let only_text = text.clone_reader(); let stack = @Stack { @@ -159,9 +156,7 @@ impl ComposeChild<'static> for TextSelectable { let high_light_rect = @ OnlySizedByParent { @ SelectedHighLight { - rects: pipe! { - $this.select_text_rect(&$text, $text.layout_size()) - } + rects: pipe! { $this.select_text_rect(&$text)} } }; let text_widget = text.into_widget(); @@ -169,14 +164,13 @@ impl ComposeChild<'static> for TextSelectable { this.clone_writer(), text_widget, only_text.clone_reader(), - layout_box.clone_reader() ); @ $stack { tab_index: -1_i16, on_blur: move |_| { $this.write().set_caret(CaretState::default()); }, on_key_down: move |k| { - select_key_handle(&this, &$only_text, &$layout_box, k); + select_key_handle(&this, &$only_text, k); }, @ $high_light_rect { } @ $text_widget {} @@ -187,7 +181,7 @@ impl ComposeChild<'static> for TextSelectable { } pub(crate) fn select_key_handle( - this: &impl StateWriter, text: &Text, text_layout: &LayoutBox, event: &KeyboardEvent, + this: &impl StateWriter, text: &Text, event: &KeyboardEvent, ) { let mut deal = false; if event.with_command_key() { @@ -195,7 +189,7 @@ pub(crate) fn select_key_handle( } if !deal { - deal_with_selection(this, text, text_layout, event); + deal_with_selection(this, text, event); } } @@ -233,14 +227,11 @@ fn is_move_by_word(event: &KeyboardEvent) -> bool { } fn deal_with_selection( - this: &impl StateWriter, text: &Text, text_layout: &LayoutBox, event: &KeyboardEvent, + this: &impl StateWriter, text: &Text, event: &KeyboardEvent, ) { - let helper = || { - TextGlyphsHelper::new( - text.text.clone(), - text.text_layout(&mut AppCtx::typography_store().borrow_mut(), text_layout.layout_size()), - ) - }; + let Some(glyphs) = text.glyphs() else { return }; + let helper = TextGlyphsHelper::new(text.text.clone(), glyphs.clone()); + let old_caret = this.read().caret(); let text = this.read().text().clone(); let new_caret_position = match event.key() { @@ -249,9 +240,9 @@ fn deal_with_selection( let cluster = select_prev_word(&text, old_caret.cluster(), false).start; Some(CaretPosition { cluster, position: None }) } else if event.with_command_key() { - helper().line_begin(&text, old_caret.caret_position()) + helper.line_begin(&text, old_caret.caret_position()) } else { - helper().prev(&text, old_caret.caret_position()) + helper.prev(&text, old_caret.caret_position()) } } VirtualKey::Named(NamedKey::ArrowRight) => { @@ -259,15 +250,15 @@ fn deal_with_selection( let cluster = select_next_word(&text, old_caret.cluster(), true).end; Some(CaretPosition { cluster, position: None }) } else if event.with_command_key() { - helper().line_end(&text, old_caret.caret_position()) + helper.line_end(&text, old_caret.caret_position()) } else { - helper().next(&text, old_caret.caret_position()) + helper.next(&text, old_caret.caret_position()) } } - VirtualKey::Named(NamedKey::ArrowUp) => helper().up(&text, old_caret.caret_position()), - VirtualKey::Named(NamedKey::ArrowDown) => helper().down(&text, old_caret.caret_position()), - VirtualKey::Named(NamedKey::Home) => helper().line_begin(&text, old_caret.caret_position()), - VirtualKey::Named(NamedKey::End) => helper().line_end(&text, old_caret.caret_position()), + VirtualKey::Named(NamedKey::ArrowUp) => helper.up(&text, old_caret.caret_position()), + VirtualKey::Named(NamedKey::ArrowDown) => helper.down(&text, old_caret.caret_position()), + VirtualKey::Named(NamedKey::Home) => helper.line_begin(&text, old_caret.caret_position()), + VirtualKey::Named(NamedKey::End) => helper.line_end(&text, old_caret.caret_position()), _ => None, }; diff --git a/widgets/src/lists.rs b/widgets/src/lists.rs index df5e0bc68..e817940ec 100644 --- a/widgets/src/lists.rs +++ b/widgets/src/lists.rs @@ -156,7 +156,7 @@ pub struct EdgeItemStyle { #[derive(Clone)] pub struct EdgeTextItemStyle { - pub style: CowArc, + pub style: TextStyle, pub gap: Option, pub foreground: Brush, } @@ -330,8 +330,8 @@ pub struct ListItemStyle { pub padding_style: Option, pub label_gap: Option, pub item_align: fn(usize) -> Align, - pub headline_style: CowArc, - pub supporting_style: CowArc, + pub headline_style: TextStyle, + pub supporting_style: TextStyle, pub leading_config: EdgeWidgetStyle, pub trailing_config: EdgeWidgetStyle, } diff --git a/widgets/src/tabs.rs b/widgets/src/tabs.rs index 1f993f8cd..faff5742d 100644 --- a/widgets/src/tabs.rs +++ b/widgets/src/tabs.rs @@ -72,7 +72,7 @@ pub struct TabsStyle { pub icon_pos: Position, pub active_color: Brush, pub foreground: Brush, - pub label_style: CowArc, + pub label_style: TextStyle, pub indicator: IndicatorStyle, } diff --git a/widgets/src/text.rs b/widgets/src/text.rs index 7b5536c64..4d737366b 100644 --- a/widgets/src/text.rs +++ b/widgets/src/text.rs @@ -1,35 +1,40 @@ +use std::cell::{Ref, RefCell}; + use ribir_core::prelude::*; +use typography::PlaceLineDirection; /// The text widget display text with a single style. -#[derive(Debug, Declare, Clone, PartialEq)] +#[derive(Declare)] pub struct Text { pub text: CowArc, - #[declare(default = TypographyTheme::of(ctx!()).body_medium.text.clone())] - pub text_style: CowArc, #[declare(default = TextAlign::Start)] pub text_align: TextAlign, -} - -impl VisualText for Text { - fn text(&self) -> CowArc { self.text.clone() } - fn text_style(&self) -> &TextStyle { &self.text_style } - fn text_align(&self) -> TextAlign { self.text_align } + #[declare(default)] + glyphs: RefCell>, } impl Render for Text { - fn perform_layout(&self, clamp: BoxClamp, _: &mut LayoutCtx) -> Size { - let size = self - .text_layout(&mut AppCtx::typography_store().borrow_mut(), clamp.max) - .visual_rect() - .size - .cast_unit(); + fn perform_layout(&self, clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size { + let style = Provider::of::(&ctx).unwrap(); + let info = AppCtx::typography_store() + .borrow_mut() + .typography( + self.text.substr(..), + &style, + clamp.max, + self.text_align, + PlaceLineDirection::TopToBottom, + ); + + let size = info.visual_rect().size; + *self.glyphs.borrow_mut() = Some(info); + clamp.clamp(size) } #[inline] fn only_sized_by_parent(&self) -> bool { false } - #[inline] fn paint(&self, ctx: &mut PaintingCtx) { let box_rect = Rect::from_size(ctx.box_size().unwrap()); if ctx @@ -40,12 +45,17 @@ impl Render for Text { return; }; - let bounds = ctx.layout_clamp().map(|b| b.max).unwrap(); - let visual_glyphs = self.text_layout(&mut AppCtx::typography_store().borrow_mut(), bounds); + let visual_glyphs = self.glyphs().unwrap(); let font_db = AppCtx::font_db().clone(); - let font_size = self.text_style.font_size; + ctx + .painter() + .draw_glyphs_in_rect(&visual_glyphs, box_rect, &font_db.borrow()); + } +} - draw_glyphs_in_rect(ctx.painter(), visual_glyphs, box_rect, font_size, font_db); +impl Text { + pub fn glyphs(&self) -> Option> { + Ref::filter_map(self.glyphs.borrow(), |v| v.as_ref()).ok() } } @@ -80,23 +90,42 @@ define_text_with_theme_style!(H6, title_small); #[cfg(test)] mod tests { use ribir_core::test_helper::*; + use ribir_dev_helper::*; use super::*; use crate::layout::SizedBox; + const WND_SIZE: Size = Size::new(164., 64.); - #[test] - fn text_clip() { - let _guard = unsafe { AppCtx::new_lock_scope() }; - - let w = fn_widget! { + widget_test_suit!( + text_clip, + WidgetTester::new(fn_widget! { @SizedBox { size: Size::new(50., 45.), @Text { text: "hello world,\rnice to meet you.", } } - }; - let wnd = TestWindow::new_with_size(w, Size::new(120., 80.)); - wnd.layout(); - } + }) + .with_wnd_size(WND_SIZE) + .with_comparison(0.000025), + LayoutCase::default().with_size(Size::new(50., 45.)) + ); + + widget_image_tests!( + default_text, + WidgetTester::new(fn_widget! { + @Text { text: "Hello ribir!"} + }) + .with_wnd_size(WND_SIZE) + .with_comparison(0.000025) + ); + + widget_image_tests!( + h1, + WidgetTester::new(fn_widget! { + @H1 { text: "Hello ribir!" } + }) + .with_wnd_size(WND_SIZE) + .with_comparison(0.000025) + ); } diff --git a/widgets/src/text_field.rs b/widgets/src/text_field.rs index 386e74784..400fdb1f3 100644 --- a/widgets/src/text_field.rs +++ b/widgets/src/text_field.rs @@ -48,7 +48,7 @@ pub struct TextFieldTheme { /// text foreground. pub text_brush: Brush, /// textfield input's text style - pub text: CowArc, + pub text: TextStyle, /// textfield's background color pub container_color: Color, @@ -64,10 +64,10 @@ pub struct TextFieldTheme { pub label_color: Color, /// label's text style when collapse - pub label_collapse: CowArc, + pub label_collapse: TextStyle, /// label's text style when expand - pub label_expand: CowArc, + pub label_expand: TextStyle, /// edit area's padding when collapse pub input_collapse_padding: EdgeInsets, @@ -136,7 +136,7 @@ impl<'c> ComposeChild<'c> for TextFieldThemeProxy { impl TextFieldThemeProxy { fn theme(&self) -> Option<&TextFieldTheme> { self.suit.get(self.state) } - fn label_style(&self, is_text_empty: bool) -> CowArc { + fn label_style(&self, is_text_empty: bool) -> TextStyle { if self.is_collapse(is_text_empty) { self.label_collapse.clone() } else { @@ -181,7 +181,7 @@ impl CustomStyle for TextFieldThemeSuit { impl TextFieldThemeSuit { pub fn from_theme(palette: &Palette, typo_theme: &TypographyTheme) -> Self { - let body: &CowArc = &typo_theme.body_large.text; + let body: &TextStyle = &typo_theme.body_large.text; let header = &typo_theme.title_large.text; let caption = &typo_theme.label_small.text; @@ -381,7 +381,7 @@ fn build_input_area( #[derive(Declare)] struct TextFieldLabel { text: CowArc, - style: CowArc, + style: TextStyle, } impl Compose for TextFieldLabel {