1use crate::ext::WidgetLayoutExt;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout::{Dimension, LayoutNode, LayoutStyle, StyleNode};
5use maycoon_core::skrifa::instance::Size;
6use maycoon_core::skrifa::raw::FileRef;
7use maycoon_core::skrifa::setting::VariationSetting;
8use maycoon_core::skrifa::MetadataProvider;
9use maycoon_core::state::{State, Val};
10use maycoon_core::vg::peniko::{Brush, Fill};
11use maycoon_core::vg::{peniko, Glyph, Scene};
12use maycoon_core::widget::Widget;
13use maycoon_theme::id::WidgetId;
14use maycoon_theme::theme::Theme;
15use nalgebra::Vector2;
16
17pub struct Text<S: State> {
28 style: Val<S, LayoutStyle>,
29 text: Val<S, String>,
30 font: Val<S, Option<String>>,
31 font_size: Val<S, f32>,
32 hinting: Val<S, bool>,
33 line_gap: Val<S, f32>,
34}
35
36impl<S: State> Text<S> {
37 pub fn new(text: impl Into<Val<S, String>>) -> Self {
39 Self {
40 style: LayoutStyle::default().into(),
41 text: text.into(),
42 font: None.into(),
43 font_size: 30.0.into(),
44 hinting: true.into(),
45 line_gap: 7.5.into(),
46 }
47 }
48
49 pub fn with_hinting(mut self, hinting: impl Into<Val<S, bool>>) -> Self {
54 self.hinting = hinting.into();
55 self
56 }
57
58 pub fn with_font(mut self, font: impl Into<Val<S, String>>) -> Self {
60 self.font = font.into().map(Some);
61 self
62 }
63
64 pub fn with_font_size(mut self, size: impl Into<Val<S, f32>>) -> Self {
66 self.font_size = size.into();
67 self
68 }
69
70 pub fn with_line_gap(mut self, gap: impl Into<Val<S, f32>>) -> Self {
74 self.line_gap = gap.into();
75 self
76 }
77}
78
79impl<S: State> WidgetLayoutExt<S> for Text<S> {
80 fn set_layout_style(&mut self, layout_style: impl Into<Val<S, LayoutStyle>>) {
81 self.style = layout_style.into();
82 }
83}
84
85impl<S: State> Widget<S> for Text<S> {
86 fn render(
87 &mut self,
88 scene: &mut Scene,
89 theme: &mut dyn Theme,
90 info: &AppInfo,
91 layout_node: &LayoutNode,
92 state: &S,
93 ) {
94 let font_size = *self.font_size.get_ref(state);
95 let hinting = *self.hinting.get_ref(state);
96 let font_name = self.font.get_ref(state);
97
98 let font = if font_name.is_some() {
99 info.font_context
100 .get(font_name.clone().unwrap())
101 .expect("Font not found")
102 } else {
103 info.font_context.default_font().clone()
104 };
105
106 let font_ref = {
107 let file_ref = FileRef::new(font.data.as_ref()).expect("Failed to load font data");
108 match file_ref {
109 FileRef::Font(font) => Some(font),
110 FileRef::Collection(collection) => collection.get(font.index).ok(),
111 }
112 }
113 .expect("Failed to load font reference");
114
115 let color = if let Some(style) = theme.of(<Text<S> as Widget<S>>::widget_id(self)) {
116 if theme.globals().invert_text_color {
117 style.get_color("color_invert").unwrap()
118 } else {
119 style.get_color("color").unwrap()
120 }
121 } else {
122 theme.defaults().text().foreground()
123 };
124
125 let location = font_ref.axes().location::<&[VariationSetting; 0]>(&[]);
126
127 let metrics = font_ref.metrics(Size::new(font_size), &location);
128
129 let glyph_metrics = font_ref.glyph_metrics(Size::new(font_size), &location);
130
131 let line_height = metrics.ascent + metrics.descent + metrics.leading;
132
133 let line_gap = *self.line_gap.get_ref(state);
134
135 let charmap = font_ref.charmap();
136
137 let mut pen_x = layout_node.layout.location.x;
138
139 let mut pen_y = layout_node.layout.location.y + font_size;
140
141 let text = self.text.get_ref(state);
142
143 scene
144 .draw_glyphs(&font)
145 .font_size(font_size)
146 .brush(&Brush::Solid(color))
147 .normalized_coords(bytemuck::cast_slice(location.coords()))
148 .hint(hinting)
149 .draw(
150 &peniko::Style::Fill(Fill::NonZero),
151 text.chars().filter_map(|c| {
152 if c == '\n' {
153 pen_y += line_height + line_gap;
154 pen_x = layout_node.layout.location.x;
155 return None;
156 }
157
158 let gid = charmap.map(c).unwrap_or_default();
159 let advance = glyph_metrics.advance_width(gid).unwrap_or_default();
160 let x = pen_x;
161
162 pen_x += advance;
163
164 Some(Glyph {
165 id: gid.to_u32(),
166 x,
167 y: pen_y,
168 })
169 }),
170 );
171 }
172
173 fn layout_style(&mut self, state: &S) -> StyleNode {
174 let text = self.text.get_ref(state);
175
176 let font_size = *self.font_size.get_ref(state);
177
178 let style = self.style.get_ref(state).clone();
179
180 StyleNode {
181 style: LayoutStyle {
182 size: Vector2::new(
183 Dimension::Length(font_size * text.len() as f32),
184 Dimension::Length(font_size),
185 ),
186 ..style
187 },
188 children: Vec::new(),
189 }
190 }
191
192 fn update(&mut self, _: &LayoutNode, _: &mut S, _: &AppInfo) -> Update {
193 self.text.invalidate();
194 self.font.invalidate();
195 self.hinting.invalidate();
196 self.font_size.invalidate();
197 self.style.invalidate();
198 Update::empty()
199 }
200
201 fn widget_id(&self) -> WidgetId {
202 WidgetId::new("maycoon-widgets", "Text")
203 }
204}