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