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 IntoUiNode) -> 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 IntoUiNode) -> 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 IntoUiNode) -> 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 IntoUiNode) -> 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 IntoUiNode) -> 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::Update { .. } => {
179 FOCUS_CHANGED_EVENT.with_new(|updates| {
181 for args in updates.iter() {
182 let new_is_focused = args.is_focus_within(TEXT.try_rich().map(|r| r.root_id).unwrap_or_else(|| WIDGET.id()));
183 if is_focused != new_is_focused {
184 WIDGET.render();
185 is_focused = new_is_focused;
186 }
187 }
188 });
189 }
190 UiNodeOp::Render { frame } => {
191 let r_txt = TEXT.resolved();
192
193 if let Some(range) = r_txt.caret.selection_range() {
194 let l_txt = TEXT.laidout();
195 let txt = r_txt.segmented_text.text();
196
197 let mut selection_color = SELECTION_COLOR_VAR.get();
198 if !is_focused && !r_txt.selection_toolbar_is_open {
199 selection_color = selection_color.desaturate(100.pct());
200 }
201
202 for line_rect in l_txt.shaped_text.highlight_rects(range, txt) {
203 if !line_rect.size.is_empty() {
204 frame.push_color(line_rect, FrameValue::Value(selection_color));
205 }
206 }
207 };
208 }
209 _ => {}
210 })
211}
212
213pub fn render_text() -> UiNode {
221 #[derive(Clone, Copy, PartialEq)]
222 struct RenderedText {
223 version: u32,
224 synthesis: FontSynthesis,
225 color: Rgba,
226 aa: FontAntiAliasing,
227 }
228
229 let mut reuse = None;
230 let mut rendered = None;
231 let mut color_key = None;
232 let image_spatial_id = SpatialFrameId::new_unique();
233 let mut has_loading_images = false;
234
235 match_node_leaf(move |op| match op {
236 UiNodeOp::Init => {
237 WIDGET
238 .sub_var_render_update(&FONT_COLOR_VAR)
239 .sub_var_render(&FONT_AA_VAR)
240 .sub_var(&FONT_PALETTE_VAR)
241 .sub_var(&FONT_PALETTE_COLORS_VAR);
242
243 if FONT_COLOR_VAR.capabilities().contains(VarCapability::NEW) {
244 color_key = Some(FrameValueKey::new_unique());
245 }
246 }
247 UiNodeOp::Deinit => {
248 color_key = None;
249 reuse = None;
250 rendered = None;
251 has_loading_images = false;
252 }
253 UiNodeOp::Update { .. } => {
254 if (FONT_PALETTE_VAR.is_new() || FONT_PALETTE_COLORS_VAR.is_new())
255 && let Some(t) = TEXT.try_laidout()
256 && t.shaped_text.has_colored_glyphs()
257 {
258 WIDGET.render();
259 }
260 }
261 UiNodeOp::Measure { desired_size, .. } => {
262 let txt = TEXT.laidout();
263 *desired_size = LAYOUT.constraints().inner().fill_size_or(txt.shaped_text.size())
264 }
265 UiNodeOp::Layout { final_size, .. } => {
266 let txt = TEXT.laidout();
269 *final_size = LAYOUT.constraints().inner().fill_size_or(txt.shaped_text.size())
270 }
271 UiNodeOp::Render { frame } => {
272 let r = TEXT.resolved();
273 let mut t = TEXT.layout();
274
275 let lh = t.shaped_text.line_height();
276 let clip = PxRect::from_size(t.shaped_text.align_size()).inflate(lh, lh); if clip.size.is_empty() {
278 return;
279 }
280
281 let color = FONT_COLOR_VAR.get();
282 let color_value = if let Some(key) = color_key {
283 key.bind(color, FONT_COLOR_VAR.is_animating())
284 } else {
285 FrameValue::Value(color)
286 };
287
288 let aa = FONT_AA_VAR.get();
289
290 let rt = Some(RenderedText {
291 version: t.shaped_text_version,
292 synthesis: r.synthesis,
293 color,
294 aa,
295 });
296 if rendered != rt {
297 rendered = rt;
298 reuse = None;
299 }
300
301 t.render_info.transform = *frame.transform();
302 t.render_info.scale_factor = frame.scale_factor();
303
304 if std::mem::take(&mut has_loading_images)
305 && reuse.is_some()
306 && frame.render_widgets().delivery_list().enter_widget(WIDGET.id())
307 {
308 reuse = None;
310 }
311
312 frame.push_reuse(&mut reuse, |frame| {
313 if t.shaped_text.has_images() {
314 let mut img_count = 0;
315 let mut push_img_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| match glyphs {
316 ShapedImageGlyphs::Normal(glyphs) => {
317 if let Some(offset) = offset {
318 let mut glyphs = glyphs.to_vec();
319 for g in &mut glyphs {
320 g.point.x += offset.x;
321 g.point.y += offset.y;
322 }
323 frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
324 } else {
325 frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
326 }
327 }
328 ShapedImageGlyphs::Image { rect, img, .. } => {
329 let is_loading = img.with(|i| {
330 if i.is_loaded() {
331 frame.push_reference_frame(
332 ReferenceFrameId::from_unique_child(image_spatial_id, img_count),
333 FrameValue::Value(PxTransform::translation(rect.origin.x, rect.origin.y)),
334 true,
335 true,
336 |frame| {
337 let size = rect.size.cast::<Px>();
338 frame.push_image(
339 PxRect::from_size(size),
340 size,
341 size,
342 PxSize::zero(),
343 i,
344 zng_view_api::ImageRendering::Pixelated,
345 );
346 },
347 );
348 img_count = img_count.wrapping_add(1);
349 }
350 i.is_loading()
351 });
352 if is_loading {
353 has_loading_images = true;
354 let id = WIDGET.id();
355 img.hook(move |args| {
356 if args.value().is_loaded() {
357 UPDATES.render(id);
358 }
359 args.value().is_loading()
360 })
361 .perm();
362 }
363 }
364 };
365
366 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
367 (Some(o), TextOverflow::Truncate(_), false) => {
368 for glyphs in &o.included_glyphs {
369 for (font, glyphs) in t.shaped_text.image_glyphs_slice(glyphs.clone()) {
370 push_img_glyphs(font, glyphs, None)
371 }
372 }
373
374 if let Some(suf) = &t.overflow_suffix {
375 let suf_offset = o.suffix_origin.to_vector().cast_unit();
376 for (font, glyphs) in suf.image_glyphs() {
377 push_img_glyphs(font, glyphs, Some(suf_offset))
378 }
379 }
380 }
381 _ => {
382 for (font, glyphs) in t.shaped_text.image_glyphs() {
384 push_img_glyphs(font, glyphs, None)
385 }
386 }
387 }
388 } else if t.shaped_text.has_colored_glyphs() || t.overflow_suffix.as_ref().map(|o| o.has_colored_glyphs()).unwrap_or(false)
389 {
390 let palette_query = FONT_PALETTE_VAR.get();
391 FONT_PALETTE_COLORS_VAR.with(|palette_colors| {
392 let mut push_font_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| {
393 let mut palette = None;
394
395 match glyphs {
396 ShapedColoredGlyphs::Normal(glyphs) => {
397 if let Some(offset) = offset {
398 let mut glyphs = glyphs.to_vec();
399 for g in &mut glyphs {
400 g.point.x += offset.x;
401 g.point.y += offset.y;
402 }
403 frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
404 } else {
405 frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
406 }
407 }
408 ShapedColoredGlyphs::Colored { point, glyphs, .. } => {
409 for (index, color_i) in glyphs.iter() {
410 let color = if let Some(color_i) = color_i {
411 if let Some(i) = palette_colors.iter().position(|(ci, _)| *ci == color_i) {
412 palette_colors[i].1
413 } else {
414 let palette = palette
417 .get_or_insert_with(|| font.face().color_palettes().palette(palette_query).unwrap());
418
419 palette.get(color_i).unwrap_or(color)
421 }
422 } else {
423 color
425 };
426
427 let mut g = GlyphInstance::new(index, point);
428 if let Some(offset) = offset {
429 g.point.x += offset.x;
430 g.point.y += offset.y;
431 }
432 frame.push_text(clip, &[g], font, FrameValue::Value(color), r.synthesis, aa);
433 }
434 }
435 }
436 };
437
438 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
439 (Some(o), TextOverflow::Truncate(_), false) => {
440 for glyphs in &o.included_glyphs {
441 for (font, glyphs) in t.shaped_text.colored_glyphs_slice(glyphs.clone()) {
442 push_font_glyphs(font, glyphs, None)
443 }
444 }
445
446 if let Some(suf) = &t.overflow_suffix {
447 let suf_offset = o.suffix_origin.to_vector().cast_unit();
448 for (font, glyphs) in suf.colored_glyphs() {
449 push_font_glyphs(font, glyphs, Some(suf_offset))
450 }
451 }
452 }
453 _ => {
454 for (font, glyphs) in t.shaped_text.colored_glyphs() {
456 push_font_glyphs(font, glyphs, None)
457 }
458 }
459 }
460 });
461 } else {
462 let mut push_font_glyphs = |font: &Font, glyphs: Cow<[GlyphInstance]>| {
465 frame.push_text(clip, glyphs.as_ref(), font, color_value, r.synthesis, aa);
466 };
467
468 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
469 (Some(o), TextOverflow::Truncate(_), false) => {
470 for glyphs in &o.included_glyphs {
471 for (font, glyphs) in t.shaped_text.glyphs_slice(glyphs.clone()) {
472 push_font_glyphs(font, Cow::Borrowed(glyphs))
473 }
474 }
475
476 if let Some(suf) = &t.overflow_suffix {
477 let suf_offset = o.suffix_origin.to_vector().cast_unit();
478 for (font, glyphs) in suf.glyphs() {
479 let mut glyphs = glyphs.to_vec();
480 for g in &mut glyphs {
481 g.point += suf_offset;
482 }
483 push_font_glyphs(font, Cow::Owned(glyphs))
484 }
485 }
486 }
487 _ => {
488 for (font, glyphs) in t.shaped_text.glyphs() {
490 push_font_glyphs(font, Cow::Borrowed(glyphs))
491 }
492 }
493 }
494 }
495 });
496 }
497 UiNodeOp::RenderUpdate { update } => {
498 TEXT.layout().render_info.transform = *update.transform();
499
500 if let Some(key) = color_key
501 && let Some(mut r) = rendered
502 {
503 let color = FONT_COLOR_VAR.get();
504
505 update.update_color(key.update(color, FONT_COLOR_VAR.is_animating()));
506
507 r.color = color;
508 rendered = Some(r);
509 }
510 }
511 _ => {}
512 })
513}