yakui_widgets/widgets/
render_text.rs1use 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#[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 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}