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.var().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); let color = FONT_COLOR_VAR.get();
278 let color_value = if let Some(key) = color_key {
279 key.bind(color, FONT_COLOR_VAR.is_animating())
280 } else {
281 FrameValue::Value(color)
282 };
283
284 let aa = FONT_AA_VAR.get();
285
286 let rt = Some(RenderedText {
287 version: t.shaped_text_version,
288 synthesis: r.synthesis,
289 color,
290 aa,
291 });
292 if rendered != rt {
293 rendered = rt;
294 reuse = None;
295 }
296
297 t.render_info.transform = *frame.transform();
298 t.render_info.scale_factor = frame.scale_factor();
299
300 if std::mem::take(&mut has_loading_images)
301 && reuse.is_some()
302 && frame.render_widgets().delivery_list().enter_widget(WIDGET.id())
303 {
304 reuse = None;
306 }
307
308 frame.push_reuse(&mut reuse, |frame| {
309 if t.shaped_text.has_images() {
310 let mut img_count = 0;
311 let mut push_img_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| match glyphs {
312 ShapedImageGlyphs::Normal(glyphs) => {
313 if let Some(offset) = offset {
314 let mut glyphs = glyphs.to_vec();
315 for g in &mut glyphs {
316 g.point.x += offset.x;
317 g.point.y += offset.y;
318 }
319 frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
320 } else {
321 frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
322 }
323 }
324 ShapedImageGlyphs::Image { rect, img, .. } => {
325 let is_loading = img.with(|i| {
326 if i.is_loaded() {
327 frame.push_reference_frame(
328 ReferenceFrameId::from_unique_child(image_spatial_id, img_count),
329 FrameValue::Value(PxTransform::translation(rect.origin.x, rect.origin.y)),
330 true,
331 true,
332 |frame| {
333 let size = rect.size.cast::<Px>();
334 frame.push_image(
335 PxRect::from_size(size),
336 size,
337 size,
338 PxSize::zero(),
339 i,
340 zng_view_api::ImageRendering::Pixelated,
341 );
342 },
343 );
344 img_count = img_count.wrapping_add(1);
345 }
346 i.is_loading()
347 });
348 if is_loading {
349 has_loading_images = true;
350 let id = WIDGET.id();
351 img.hook(move |args| {
352 if args.value().is_loaded() {
353 UPDATES.render(id);
354 }
355 args.value().is_loading()
356 })
357 .perm();
358 }
359 }
360 };
361
362 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
363 (Some(o), TextOverflow::Truncate(_), false) => {
364 for glyphs in &o.included_glyphs {
365 for (font, glyphs) in t.shaped_text.image_glyphs_slice(glyphs.clone()) {
366 push_img_glyphs(font, glyphs, None)
367 }
368 }
369
370 if let Some(suf) = &t.overflow_suffix {
371 let suf_offset = o.suffix_origin.to_vector().cast_unit();
372 for (font, glyphs) in suf.image_glyphs() {
373 push_img_glyphs(font, glyphs, Some(suf_offset))
374 }
375 }
376 }
377 _ => {
378 for (font, glyphs) in t.shaped_text.image_glyphs() {
380 push_img_glyphs(font, glyphs, None)
381 }
382 }
383 }
384 } else if t.shaped_text.has_colored_glyphs() || t.overflow_suffix.as_ref().map(|o| o.has_colored_glyphs()).unwrap_or(false)
385 {
386 let palette_query = FONT_PALETTE_VAR.get();
387 FONT_PALETTE_COLORS_VAR.with(|palette_colors| {
388 let mut push_font_glyphs = |font: &Font, glyphs, offset: Option<euclid::Vector2D<f32, Px>>| {
389 let mut palette = None;
390
391 match glyphs {
392 ShapedColoredGlyphs::Normal(glyphs) => {
393 if let Some(offset) = offset {
394 let mut glyphs = glyphs.to_vec();
395 for g in &mut glyphs {
396 g.point.x += offset.x;
397 g.point.y += offset.y;
398 }
399 frame.push_text(clip, &glyphs, font, color_value, r.synthesis, aa);
400 } else {
401 frame.push_text(clip, glyphs, font, color_value, r.synthesis, aa);
402 }
403 }
404 ShapedColoredGlyphs::Colored { point, glyphs, .. } => {
405 for (index, color_i) in glyphs.iter() {
406 let color = if let Some(color_i) = color_i {
407 if let Some(i) = palette_colors.iter().position(|(ci, _)| *ci == color_i) {
408 palette_colors[i].1
409 } else {
410 let palette = palette
413 .get_or_insert_with(|| font.face().color_palettes().palette(palette_query).unwrap());
414
415 palette.get(color_i).unwrap_or(color)
417 }
418 } else {
419 color
421 };
422
423 let mut g = GlyphInstance::new(index, point);
424 if let Some(offset) = offset {
425 g.point.x += offset.x;
426 g.point.y += offset.y;
427 }
428 frame.push_text(clip, &[g], font, FrameValue::Value(color), r.synthesis, aa);
429 }
430 }
431 }
432 };
433
434 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
435 (Some(o), TextOverflow::Truncate(_), false) => {
436 for glyphs in &o.included_glyphs {
437 for (font, glyphs) in t.shaped_text.colored_glyphs_slice(glyphs.clone()) {
438 push_font_glyphs(font, glyphs, None)
439 }
440 }
441
442 if let Some(suf) = &t.overflow_suffix {
443 let suf_offset = o.suffix_origin.to_vector().cast_unit();
444 for (font, glyphs) in suf.colored_glyphs() {
445 push_font_glyphs(font, glyphs, Some(suf_offset))
446 }
447 }
448 }
449 _ => {
450 for (font, glyphs) in t.shaped_text.colored_glyphs() {
452 push_font_glyphs(font, glyphs, None)
453 }
454 }
455 }
456 });
457 } else {
458 let mut push_font_glyphs = |font: &Font, glyphs: Cow<[GlyphInstance]>| {
461 frame.push_text(clip, glyphs.as_ref(), font, color_value, r.synthesis, aa);
462 };
463
464 match (&t.overflow, TEXT_OVERFLOW_VAR.get(), TEXT_EDITABLE_VAR.get()) {
465 (Some(o), TextOverflow::Truncate(_), false) => {
466 for glyphs in &o.included_glyphs {
467 for (font, glyphs) in t.shaped_text.glyphs_slice(glyphs.clone()) {
468 push_font_glyphs(font, Cow::Borrowed(glyphs))
469 }
470 }
471
472 if let Some(suf) = &t.overflow_suffix {
473 let suf_offset = o.suffix_origin.to_vector().cast_unit();
474 for (font, glyphs) in suf.glyphs() {
475 let mut glyphs = glyphs.to_vec();
476 for g in &mut glyphs {
477 g.point += suf_offset;
478 }
479 push_font_glyphs(font, Cow::Owned(glyphs))
480 }
481 }
482 }
483 _ => {
484 for (font, glyphs) in t.shaped_text.glyphs() {
486 push_font_glyphs(font, Cow::Borrowed(glyphs))
487 }
488 }
489 }
490 }
491 });
492 }
493 UiNodeOp::RenderUpdate { update } => {
494 TEXT.layout().render_info.transform = *update.transform();
495
496 if let Some(key) = color_key {
497 let color = FONT_COLOR_VAR.get();
498
499 update.update_color(key.update(color, FONT_COLOR_VAR.is_animating()));
500
501 let mut r = rendered.unwrap();
502 r.color = color;
503 rendered = Some(r);
504 }
505 }
506 _ => {}
507 })
508}