yakui_widgets/widgets/
render_text.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::fmt;
4
5use fontdue::layout::{
6    CoordinateSystem, HorizontalAlign as FontdueAlign, Layout, LayoutSettings,
7    TextStyle as FontdueTextStyle,
8};
9use yakui_core::geometry::{Color, Constraints, Rect, Vec2};
10use yakui_core::paint::{PaintRect, Pipeline};
11use yakui_core::widget::{LayoutContext, PaintContext, Widget};
12use yakui_core::Response;
13
14use crate::font::{FontName, Fonts};
15use crate::style::{TextAlignment, TextStyle};
16use crate::text_renderer::TextGlobalState;
17use crate::util::widget;
18
19/**
20Renders text. You probably want to use [Text][super::Text] instead, which
21supports features like padding.
22*/
23#[derive(Debug)]
24#[non_exhaustive]
25#[must_use = "yakui widgets do nothing if you don't `show` them"]
26pub struct RenderText {
27    pub text: Cow<'static, str>,
28    pub style: TextStyle,
29}
30
31impl RenderText {
32    pub fn new(size: f32, text: Cow<'static, str>) -> Self {
33        let mut style = TextStyle::label();
34        style.font_size = size;
35
36        Self { text, style }
37    }
38
39    pub fn label(text: Cow<'static, str>) -> Self {
40        Self {
41            text,
42            style: TextStyle::label(),
43        }
44    }
45
46    pub fn show(self) -> Response<RenderTextResponse> {
47        widget::<RenderTextWidget>(self)
48    }
49}
50
51pub struct RenderTextWidget {
52    props: RenderText,
53    layout: RefCell<Layout>,
54}
55
56pub type RenderTextResponse = ();
57
58impl Widget for RenderTextWidget {
59    type Props<'a> = RenderText;
60    type Response = RenderTextResponse;
61
62    fn new() -> Self {
63        let layout = Layout::new(CoordinateSystem::PositiveYDown);
64
65        Self {
66            props: RenderText::new(0.0, Cow::Borrowed("")),
67            layout: RefCell::new(layout),
68        }
69    }
70
71    fn update(&mut self, props: Self::Props<'_>) -> Self::Response {
72        self.props = props;
73    }
74
75    fn layout(&self, ctx: LayoutContext<'_>, input: Constraints) -> Vec2 {
76        let fonts = ctx.dom.get_global_or_init(Fonts::default);
77
78        let font = match fonts.get(&self.props.style.font) {
79            Some(font) => font,
80            None => {
81                // TODO: Log once that we were unable to find this font.
82                panic!(
83                    "font `{}` was set, but was not registered",
84                    self.props.style.font
85                );
86            }
87        };
88
89        let max_width = input
90            .max
91            .x
92            .is_finite()
93            .then_some(input.max.x * ctx.layout.scale_factor());
94        let max_height = input
95            .max
96            .y
97            .is_finite()
98            .then_some(input.max.y * ctx.layout.scale_factor());
99
100        let horizontal_align = match self.props.style.align {
101            TextAlignment::Start => FontdueAlign::Left,
102            TextAlignment::Center => FontdueAlign::Center,
103            TextAlignment::End => FontdueAlign::Right,
104        };
105
106        let mut text_layout = self.layout.borrow_mut();
107        text_layout.reset(&LayoutSettings {
108            max_width,
109            max_height,
110            horizontal_align,
111            ..LayoutSettings::default()
112        });
113
114        text_layout.append(
115            &[&*font],
116            &FontdueTextStyle::new(
117                &self.props.text,
118                (self.props.style.font_size * ctx.layout.scale_factor()).ceil(),
119                0,
120            ),
121        );
122
123        let offset_x = get_text_layout_offset_x(&text_layout, ctx.layout.scale_factor());
124
125        let size = get_text_layout_size(&text_layout, ctx.layout.scale_factor())
126            - Vec2::new(offset_x, 0.0);
127
128        input.constrain_min(size)
129    }
130
131    fn paint(&self, mut ctx: PaintContext<'_>) {
132        let text_layout = self.layout.borrow_mut();
133        let offset_x = get_text_layout_offset_x(&text_layout, ctx.layout.scale_factor());
134        let layout_node = ctx.layout.get(ctx.dom.current()).unwrap();
135
136        paint_text(
137            &mut ctx,
138            &self.props.style.font,
139            layout_node.rect.pos() - Vec2::new(offset_x, 0.0),
140            &text_layout,
141            self.props.style.color,
142        );
143    }
144}
145
146impl fmt::Debug for RenderTextWidget {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("TextComponent")
149            .field("props", &self.props)
150            .field("layout", &"(no debug impl)")
151            .finish()
152    }
153}
154
155pub(crate) fn get_text_layout_offset_x(text_layout: &Layout, scale_factor: f32) -> f32 {
156    let offset_x = text_layout
157        .glyphs()
158        .iter()
159        .map(|glyph| glyph.x)
160        .min_by(|a, b| a.total_cmp(b))
161        .unwrap_or_default();
162
163    offset_x / scale_factor
164}
165
166pub(crate) fn get_text_layout_size(text_layout: &Layout, scale_factor: f32) -> Vec2 {
167    let height = text_layout
168        .lines()
169        .iter()
170        .flat_map(|line_pos_vec| line_pos_vec.iter())
171        .map(|line| line.baseline_y - line.min_descent)
172        .max_by(|a, b| a.total_cmp(b))
173        .unwrap_or_default();
174
175    let width = text_layout
176        .glyphs()
177        .iter()
178        .map(|glyph| glyph.x + glyph.width as f32)
179        .max_by(|a, b| a.total_cmp(b))
180        .unwrap_or_default();
181
182    Vec2::new(width, height) / scale_factor
183}
184
185pub fn paint_text(
186    ctx: &mut PaintContext<'_>,
187    font: &FontName,
188    pos: Vec2,
189    text_layout: &Layout,
190    color: Color,
191) {
192    let pos = pos.round();
193    let fonts = ctx.dom.get_global_or_init(Fonts::default);
194    let font = match fonts.get(font) {
195        Some(font) => font,
196        None => return,
197    };
198
199    let text_global = ctx.dom.get_global_or_init(TextGlobalState::new);
200    let mut glyph_cache = text_global.glyph_cache.borrow_mut();
201    glyph_cache.ensure_texture(ctx.paint);
202
203    for glyph in text_layout.glyphs() {
204        let tex_rect = glyph_cache
205            .get_or_insert(ctx.paint, &font, glyph.key)
206            .as_rect()
207            .div_vec2(glyph_cache.texture_size.as_vec2());
208
209        let size = Vec2::new(glyph.width as f32, glyph.height as f32) / ctx.layout.scale_factor();
210        let pos = pos + Vec2::new(glyph.x, glyph.y) / ctx.layout.scale_factor();
211
212        let mut rect = PaintRect::new(Rect::from_pos_size(pos, size));
213        rect.color = color;
214        rect.texture = Some((glyph_cache.texture.unwrap().into(), tex_rect));
215        rect.pipeline = Pipeline::Text;
216        rect.add(ctx.paint);
217    }
218}