1use std::borrow::Cow;
2
3use zng_app::{
4 render::{FontSynthesis, FrameValueKey, ReferenceFrameId},
5 widget::{
6 WIDGET,
7 border::{LineOrientation, LineStyle},
8 node::{UiNode, UiNodeOp, match_node, match_node_leaf},
9 },
10};
11use zng_color::Rgba;
12use zng_ext_font::{Font, ShapedColoredGlyphs, ShapedImageGlyphs};
13use zng_ext_input::focus::FOCUS_CHANGED_EVENT;
14use zng_layout::{
15 context::LAYOUT,
16 unit::{Px, PxRect, PxSize},
17};
18use zng_view_api::{config::FontAntiAliasing, display_list::FrameValue, font::GlyphInstance};
19use zng_wgt::prelude::*;
20
21use crate::{
22 FONT_AA_VAR, FONT_COLOR_VAR, FONT_PALETTE_COLORS_VAR, FONT_PALETTE_VAR, IME_UNDERLINE_STYLE_VAR, OVERLINE_COLOR_VAR,
23 OVERLINE_STYLE_VAR, SELECTION_COLOR_VAR, STRIKETHROUGH_COLOR_VAR, STRIKETHROUGH_STYLE_VAR, TEXT_EDITABLE_VAR, TEXT_OVERFLOW_VAR,
24 TextOverflow, UNDERLINE_COLOR_VAR, UNDERLINE_STYLE_VAR,
25};
26
27use super::TEXT;
28
29pub fn render_underlines(child: impl UiNode) -> impl UiNode {
37 match_node(child, move |_, op| match op {
38 UiNodeOp::Init => {
39 WIDGET.sub_var_render(&UNDERLINE_STYLE_VAR).sub_var_render(&UNDERLINE_COLOR_VAR);
40 }
41 UiNodeOp::Render { frame } => {
42 let t = TEXT.laidout();
43
44 if !t.underlines.is_empty() {
45 let style = UNDERLINE_STYLE_VAR.get();
46 if style != LineStyle::Hidden {
47 let color = UNDERLINE_COLOR_VAR.get();
48 for &(origin, width) in &t.underlines {
49 frame.push_line(
50 PxRect::new(origin, PxSize::new(width, t.underline_thickness)),
51 LineOrientation::Horizontal,
52 color,
53 style,
54 );
55 }
56 }
57 }
58 }
59 _ => {}
60 })
61}
62
63pub fn render_ime_preview_underlines(child: impl UiNode) -> impl UiNode {
72 match_node(child, move |_, op| match op {
73 UiNodeOp::Init => {
74 WIDGET.sub_var_render(&IME_UNDERLINE_STYLE_VAR).sub_var_render(&FONT_COLOR_VAR);
75 }
76 UiNodeOp::Render { frame } => {
77 let t = TEXT.laidout();
78
79 if !t.ime_underlines.is_empty() {
80 let style = IME_UNDERLINE_STYLE_VAR.get();
81 if style != LineStyle::Hidden {
82 let color = FONT_COLOR_VAR.get();
83 for &(origin, width) in &t.ime_underlines {
84 frame.push_line(
85 PxRect::new(origin, PxSize::new(width, t.ime_underline_thickness)),
86 LineOrientation::Horizontal,
87 color,
88 style,
89 );
90 }
91 }
92 }
93 }
94 _ => {}
95 })
96}
97
98pub fn render_strikethroughs(child: impl UiNode) -> impl UiNode {
106 match_node(child, move |_, op| match op {
107 UiNodeOp::Init => {
108 WIDGET
109 .sub_var_render(&STRIKETHROUGH_STYLE_VAR)
110 .sub_var_render(&STRIKETHROUGH_COLOR_VAR);
111 }
112 UiNodeOp::Render { frame } => {
113 let t = TEXT.laidout();
114 if !t.strikethroughs.is_empty() {
115 let style = STRIKETHROUGH_STYLE_VAR.get();
116 if style != LineStyle::Hidden {
117 let color = STRIKETHROUGH_COLOR_VAR.get();
118 for &(origin, width) in &t.strikethroughs {
119 frame.push_line(
120 PxRect::new(origin, PxSize::new(width, t.strikethrough_thickness)),
121 LineOrientation::Horizontal,
122 color,
123 style,
124 );
125 }
126 }
127 }
128 }
129 _ => {}
130 })
131}
132
133pub fn render_overlines(child: impl UiNode) -> impl UiNode {
141 match_node(child, move |_, op| match op {
142 UiNodeOp::Init => {
143 WIDGET.sub_var_render(&OVERLINE_STYLE_VAR).sub_var_render(&OVERLINE_COLOR_VAR);
144 }
145 UiNodeOp::Render { frame } => {
146 let t = TEXT.laidout();
147 if !t.overlines.is_empty() {
148 let style = OVERLINE_STYLE_VAR.get();
149 if style != LineStyle::Hidden {
150 let color = OVERLINE_COLOR_VAR.get();
151 for &(origin, width) in &t.overlines {
152 frame.push_line(
153 PxRect::new(origin, PxSize::new(width, t.overline_thickness)),
154 LineOrientation::Horizontal,
155 color,
156 style,
157 );
158 }
159 }
160 }
161 }
162 _ => {}
163 })
164}
165
166pub fn render_selection(child: impl UiNode) -> impl UiNode {
172 let mut is_focused = false;
173 match_node(child, move |_, op| match op {
174 UiNodeOp::Init => {
175 WIDGET.sub_var_render(&SELECTION_COLOR_VAR);
176 is_focused = false;
177 }
178 UiNodeOp::Event { update } => {
179 if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
180 let new_is_focused = args.is_focus_within(WIDGET.id());
181 if is_focused != new_is_focused {
182 WIDGET.render();
183 is_focused = new_is_focused;
184 }
185 }
186 }
187 UiNodeOp::Render { frame } => {
188 let r_txt = TEXT.resolved();
189
190 if let Some(range) = r_txt.caret.selection_range() {
191 let l_txt = TEXT.laidout();
192 let txt = r_txt.segmented_text.text();
193
194 let mut selection_color = SELECTION_COLOR_VAR.get();
195 if !is_focused && !r_txt.selection_toolbar_is_open {
196 selection_color = selection_color.desaturate(100.pct());
197 }
198
199 for line_rect in l_txt.shaped_text.highlight_rects(range, txt) {
200 if !line_rect.size.is_empty() {
201 frame.push_color(line_rect, FrameValue::Value(selection_color));
202 }
203 }
204 };
205 }
206 _ => {}
207 })
208}
209
210pub fn render_text() -> impl UiNode {
218 #[derive(Clone, Copy, PartialEq)]
219 struct RenderedText {
220 version: u32,
221 synthesis: FontSynthesis,
222 color: Rgba,
223 aa: FontAntiAliasing,
224 }
225
226 let mut reuse = None;
227 let mut rendered = None;
228 let mut color_key = None;
229 let image_spatial_id = SpatialFrameId::new_unique();
230 let mut has_loading_images = false;
231
232 match_node_leaf(move |op| match op {
233 UiNodeOp::Init => {
234 WIDGET
235 .sub_var_render_update(&FONT_COLOR_VAR)
236 .sub_var_render(&FONT_AA_VAR)
237 .sub_var(&FONT_PALETTE_VAR)
238 .sub_var(&FONT_PALETTE_COLORS_VAR);
239
240 if FONT_COLOR_VAR.capabilities().contains(VarCapability::NEW) {
241 color_key = Some(FrameValueKey::new_unique());
242 }
243 }
244 UiNodeOp::Deinit => {
245 color_key = None;
246 reuse = None;
247 rendered = None;
248 has_loading_images = false;
249 }
250 UiNodeOp::Update { .. } => {
251 if FONT_PALETTE_VAR.is_new() || FONT_PALETTE_COLORS_VAR.is_new() {
252 if let Some(t) = TEXT.try_laidout() {
253 if t.shaped_text.has_colored_glyphs() {
254 WIDGET.render();
255 }
256 }
257 }
258 }
259 UiNodeOp::Measure { desired_size, .. } => {
260 let txt = TEXT.laidout();
261 *desired_size = LAYOUT.constraints().fill_size_or(txt.shaped_text.size())
262 }
263 UiNodeOp::Layout { final_size, .. } => {
264 let txt = TEXT.laidout();
267 *final_size = LAYOUT.constraints().fill_size_or(txt.shaped_text.size())
268 }
269 UiNodeOp::Render { frame } => {
270 let r = TEXT.resolved();
271 let mut t = TEXT.layout();
272
273 let lh = t.shaped_text.line_height();
274 let clip = PxRect::from_size(t.shaped_text.align_size()).inflate(lh, lh); let color = FONT_COLOR_VAR.get();
276 let color_value = if let Some(key) = color_key {
277 key.bind(color, FONT_COLOR_VAR.is_animating())
278 } else {
279 FrameValue::Value(color)
280 };
281
282 let aa = FONT_AA_VAR.get();
283
284 let rt = Some(RenderedText {
285 version: t.shaped_text_version,
286 synthesis: r.synthesis,
287 color,
288 aa,
289 });
290 if rendered != rt {
291 rendered = rt;
292 reuse = None;
293 }
294
295 t.render_info.transform = *frame.transform();
296 t.render_info.scale_factor = frame.scale_factor();
297
298 if std::mem::take(&mut has_loading_images)
299 && reuse.is_some()
300 && frame.render_widgets().delivery_list().enter_widget(WIDGET.id())
301 {
302 reuse = None;
304 }
305
306 frame.push_reuse(&mut reuse, |frame| {
307 if t.shaped_text.has_images() {
308 let mut img_count = 0;
309 let mut push_img_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| match glyphs {
310 ShapedImageGlyphs::Normal(glyphs) => {
311 if let Some(offset) = offset {
312 let mut glyphs = glyphs.to_vec();
313 for g in &mut glyphs {
314 g.point.x += offset.x;
315 g.point.y += offset.y;
316 }
317 frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
318 } else {
319 frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
320 }
321 }
322 ShapedImageGlyphs::Image { rect, img, .. } => {
323 let is_loading = img.with(|i| {
324 if i.is_loaded() {
325 frame.push_reference_frame(
326 ReferenceFrameId::from_unique_child(image_spatial_id, img_count),
327 FrameValue::Value(PxTransform::translation(rect.origin.x, rect.origin.y)),
328 true,
329 true,
330 |frame| {
331 let size = rect.size.cast::<Px>();
332 frame.push_image(
333 PxRect::from_size(size),
334 size,
335 size,
336 PxSize::zero(),
337 i,
338 zng_view_api::ImageRendering::Pixelated,
339 );
340 },
341 );
342 img_count = img_count.wrapping_add(1);
343 }
344 i.is_loading()
345 });
346 if is_loading {
347 has_loading_images = true;
348 let id = WIDGET.id();
349 img.hook(move |args| {
350 if args.value().is_loaded() {
351 UPDATES.render(id);
352 }
353 args.value().is_loading()
354 })
355 .perm();
356 }
357 }
358 };
359
360 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
361 (Some(o), TextOverflow::Truncate(_), false) => {
362 for glyphs in &o.included_glyphs {
363 for (font, glyphs) in t.shaped_text.image_glyphs_slice(glyphs.clone()) {
364 push_img_glyphs(font, glyphs, None)
365 }
366 }
367
368 if let Some(suf) = &t.overflow_suffix {
369 let suf_offset = o.suffix_origin.to_vector().cast_unit();
370 for (font, glyphs) in suf.image_glyphs() {
371 push_img_glyphs(font, glyphs, Some(suf_offset))
372 }
373 }
374 }
375 _ => {
376 for (font, glyphs) in t.shaped_text.image_glyphs() {
378 push_img_glyphs(font, glyphs, None)
379 }
380 }
381 }
382 } else if t.shaped_text.has_colored_glyphs() || t.overflow_suffix.as_ref().map(|o| o.has_colored_glyphs()).unwrap_or(false)
383 {
384 let palette_query = FONT_PALETTE_VAR.get();
385 FONT_PALETTE_COLORS_VAR.with(|palette_colors| {
386 let mut push_font_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| {
387 let mut palette = None;
388
389 match glyphs {
390 ShapedColoredGlyphs::Normal(glyphs) => {
391 if let Some(offset) = offset {
392 let mut glyphs = glyphs.to_vec();
393 for g in &mut glyphs {
394 g.point.x += offset.x;
395 g.point.y += offset.y;
396 }
397 frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
398 } else {
399 frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
400 }
401 }
402 ShapedColoredGlyphs::Colored { point, glyphs, .. } => {
403 for (index, color_i) in glyphs.iter() {
404 let color = if let Some(color_i) = color_i {
405 if let Some(i) = palette_colors.iter().position(|(ci, _)| *ci == color_i as u16) {
406 palette_colors[i].1
407 } else {
408 let palette = palette
411 .get_or_insert_with(|| font.face().color_palettes().palette(palette_query).unwrap());
412
413 palette.colors.get(color_i).copied().unwrap_or(color)
415 }
416 } else {
417 color
419 };
420
421 let mut g = GlyphInstance { point, index };
422 if let Some(offset) = offset {
423 g.point.x += offset.x;
424 g.point.y += offset.y;
425 }
426 frame.push_text(clip, &[g], font, FrameValue::Value(color), r.synthesis, aa);
427 }
428 }
429 }
430 };
431
432 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
433 (Some(o), TextOverflow::Truncate(_), false) => {
434 for glyphs in &o.included_glyphs {
435 for (font, glyphs) in t.shaped_text.colored_glyphs_slice(glyphs.clone()) {
436 push_font_glyphs(font, glyphs, None)
437 }
438 }
439
440 if let Some(suf) = &t.overflow_suffix {
441 let suf_offset = o.suffix_origin.to_vector().cast_unit();
442 for (font, glyphs) in suf.colored_glyphs() {
443 push_font_glyphs(font, glyphs, Some(suf_offset))
444 }
445 }
446 }
447 _ => {
448 for (font, glyphs) in t.shaped_text.colored_glyphs() {
450 push_font_glyphs(font, glyphs, None)
451 }
452 }
453 }
454 });
455 } else {
456 let mut push_font_glyphs = |font: &Font, glyphs: Cow<[GlyphInstance]>| {
459 frame.push_text(clip, glyphs.as_ref(), font, color_value, r.synthesis, aa);
460 };
461
462 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
463 (Some(o), TextOverflow::Truncate(_), false) => {
464 for glyphs in &o.included_glyphs {
465 for (font, glyphs) in t.shaped_text.glyphs_slice(glyphs.clone()) {
466 push_font_glyphs(font, Cow::Borrowed(glyphs))
467 }
468 }
469
470 if let Some(suf) = &t.overflow_suffix {
471 let suf_offset = o.suffix_origin.to_vector().cast_unit();
472 for (font, glyphs) in suf.glyphs() {
473 let mut glyphs = glyphs.to_vec();
474 for g in &mut glyphs {
475 g.point += suf_offset;
476 }
477 push_font_glyphs(font, Cow::Owned(glyphs))
478 }
479 }
480 }
481 _ => {
482 for (font, glyphs) in t.shaped_text.glyphs() {
484 push_font_glyphs(font, Cow::Borrowed(glyphs))
485 }
486 }
487 }
488 }
489 });
490 }
491 UiNodeOp::RenderUpdate { update } => {
492 TEXT.layout().render_info.transform = *update.transform();
493
494 if let Some(key) = color_key {
495 let color = FONT_COLOR_VAR.get();
496
497 update.update_color(key.update(color, FONT_COLOR_VAR.is_animating()));
498
499 let mut r = rendered.unwrap();
500 r.color = color;
501 rendered = Some(r);
502 }
503 }
504 _ => {}
505 })
506}