1use std::{fmt, num::NonZeroU32, time::Duration};
2
3use zng_app::render::FontSynthesis;
4use zng_color::COLOR_SCHEME_VAR;
5use zng_ext_font::{font_features::*, *};
6use zng_ext_l10n::{LANG_VAR, Langs};
7use zng_view_api::config::FontAntiAliasing;
8use zng_wgt::prelude::*;
9use zng_wgt_layer::AnchorOffset;
10
11use crate::node::TEXT;
12
13#[widget_mixin]
21pub struct FontMix<P>(P);
22
23context_var! {
24 pub static FONT_FAMILY_VAR: FontNames = FontNames::default();
28
29 pub static FONT_SIZE_VAR: FontSize = FontSize::Pt(11.0);
33
34 pub static FONT_WEIGHT_VAR: FontWeight = FontWeight::NORMAL;
38
39 pub static FONT_STYLE_VAR: FontStyle = FontStyle::Normal;
43
44 pub static FONT_STRETCH_VAR: FontStretch = FontStretch::NORMAL;
48
49 pub static FONT_SYNTHESIS_VAR: FontSynthesis = FontSynthesis::ENABLED;
53
54 pub static FONT_AA_VAR: FontAntiAliasing = FontAntiAliasing::Default;
58}
59
60impl FontMix<()> {
61 pub fn context_vars_set(set: &mut ContextValueSet) {
63 set.insert(&FONT_FAMILY_VAR);
64 set.insert(&FONT_SIZE_VAR);
65 set.insert(&FONT_WEIGHT_VAR);
66 set.insert(&FONT_STYLE_VAR);
67 set.insert(&FONT_STRETCH_VAR);
68 set.insert(&FONT_SYNTHESIS_VAR);
69 set.insert(&FONT_AA_VAR);
70 }
71}
72
73#[property(CONTEXT, default(FONT_FAMILY_VAR), widget_impl(FontMix<P>))]
85pub fn font_family(child: impl IntoUiNode, names: impl IntoVar<FontNames>) -> UiNode {
86 with_context_var(child, FONT_FAMILY_VAR, names)
87}
88
89#[property(CONTEXT, default(FONT_SIZE_VAR), widget_impl(FontMix<P>))]
98pub fn font_size(child: impl IntoUiNode, size: impl IntoVar<FontSize>) -> UiNode {
99 let child = match_node(child, |child, op| match op {
100 UiNodeOp::Init => {
101 WIDGET.sub_var_layout(&FONT_SIZE_VAR);
102 }
103 UiNodeOp::Measure { wm, desired_size } => {
104 let font_size = FONT_SIZE_VAR.get();
105 let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
106 *desired_size = if font_size_px >= 0 {
107 LAYOUT.with_font_size(font_size_px, || child.measure(wm))
108 } else {
109 tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
110 child.measure(wm)
111 };
112 }
113 UiNodeOp::Layout { wl, final_size } => {
114 let font_size = FONT_SIZE_VAR.get();
115 let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
116 *final_size = if font_size_px >= 0 {
117 LAYOUT.with_font_size(font_size_px, || child.layout(wl))
118 } else {
119 tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
120 child.layout(wl)
121 };
122 }
123 _ => {}
124 });
125 with_context_var(child, FONT_SIZE_VAR, size)
126}
127
128#[property(CONTEXT, default(FONT_WEIGHT_VAR), widget_impl(FontMix<P>))]
134pub fn font_weight(child: impl IntoUiNode, weight: impl IntoVar<FontWeight>) -> UiNode {
135 with_context_var(child, FONT_WEIGHT_VAR, weight)
136}
137
138#[property(CONTEXT, default(FONT_STYLE_VAR), widget_impl(FontMix<P>))]
144pub fn font_style(child: impl IntoUiNode, style: impl IntoVar<FontStyle>) -> UiNode {
145 with_context_var(child, FONT_STYLE_VAR, style)
146}
147
148#[property(CONTEXT, default(FONT_STRETCH_VAR), widget_impl(FontMix<P>))]
154pub fn font_stretch(child: impl IntoUiNode, stretch: impl IntoVar<FontStretch>) -> UiNode {
155 with_context_var(child, FONT_STRETCH_VAR, stretch)
156}
157
158#[property(CONTEXT, default(FONT_SYNTHESIS_VAR), widget_impl(FontMix<P>))]
168pub fn font_synthesis(child: impl IntoUiNode, enabled: impl IntoVar<FontSynthesis>) -> UiNode {
169 with_context_var(child, FONT_SYNTHESIS_VAR, enabled)
170}
171
172#[property(CONTEXT, default(FONT_AA_VAR), widget_impl(FontMix<P>))]
178pub fn font_aa(child: impl IntoUiNode, aa: impl IntoVar<FontAntiAliasing>) -> UiNode {
179 with_context_var(child, FONT_AA_VAR, aa)
180}
181
182#[widget_mixin]
188pub struct TextFillMix<P>(P);
189
190context_var! {
191 pub static FONT_COLOR_VAR: Rgba = COLOR_SCHEME_VAR.map(|s| match s {
195 ColorScheme::Light => colors::BLACK,
196 ColorScheme::Dark => colors::WHITE,
197 _ => colors::BLACK,
198 });
199
200 pub static FONT_PALETTE_VAR: FontColorPalette = COLOR_SCHEME_VAR.map_into();
204
205 pub static FONT_PALETTE_COLORS_VAR: Vec<(u16, Rgba)> = vec![];
207}
208
209impl TextFillMix<()> {
210 pub fn context_vars_set(set: &mut ContextValueSet) {
212 set.insert(&FONT_COLOR_VAR);
213 set.insert(&FONT_PALETTE_VAR);
214 set.insert(&FONT_PALETTE_COLORS_VAR);
215 }
216}
217
218#[property(CONTEXT, default(FONT_COLOR_VAR), widget_impl(TextFillMix<P>))]
227pub fn font_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
228 with_context_var(child, FONT_COLOR_VAR, color)
229}
230
231#[property(CONTEXT, default(FONT_PALETTE_VAR), widget_impl(TextFillMix<P>))]
241pub fn font_palette(child: impl IntoUiNode, palette: impl IntoVar<FontColorPalette>) -> UiNode {
242 with_context_var(child, FONT_PALETTE_VAR, palette)
243}
244
245pub fn with_font_palette_color(child: impl IntoUiNode, index: u16, color: impl IntoVar<Rgba>) -> UiNode {
254 with_context_var(
255 child,
256 FONT_PALETTE_COLORS_VAR,
257 merge_var!(FONT_PALETTE_COLORS_VAR, color.into_var(), move |set, color| {
258 let mut set = set.clone();
259 if let Some(i) = set.iter().position(|(i, _)| *i == index) {
260 set[i].1 = *color;
261 } else {
262 set.push((index, *color));
263 }
264 set
265 }),
266 )
267}
268
269#[property(CONTEXT, default(FONT_PALETTE_COLORS_VAR), widget_impl(TextFillMix<P>))]
278pub fn font_palette_colors(child: impl IntoUiNode, colors: impl IntoVar<Vec<(u16, Rgba)>>) -> UiNode {
279 with_context_var(child, FONT_PALETTE_COLORS_VAR, colors)
280}
281
282#[widget_mixin]
288pub struct TextAlignMix<P>(P);
289
290context_var! {
291 pub static TEXT_ALIGN_VAR: Align = Align::START;
293
294 pub static TEXT_OVERFLOW_ALIGN_VAR: Align = Align::TOP_START;
296
297 pub static JUSTIFY_MODE_VAR: Justify = Justify::Auto;
299}
300
301impl TextAlignMix<()> {
302 pub fn context_vars_set(set: &mut ContextValueSet) {
304 set.insert(&TEXT_ALIGN_VAR);
305 set.insert(&TEXT_OVERFLOW_ALIGN_VAR);
306 set.insert(&JUSTIFY_MODE_VAR);
307 }
308}
309
310#[property(CONTEXT, default(TEXT_ALIGN_VAR), widget_impl(TextAlignMix<P>))]
325pub fn txt_align(child: impl IntoUiNode, mode: impl IntoVar<Align>) -> UiNode {
326 with_context_var(child, TEXT_ALIGN_VAR, mode)
327}
328
329#[property(CONTEXT, default(TEXT_OVERFLOW_ALIGN_VAR), widget_impl(TextAlignMix<P>))]
342pub fn txt_overflow_align(child: impl IntoUiNode, mode: impl IntoVar<Align>) -> UiNode {
343 with_context_var(child, TEXT_OVERFLOW_ALIGN_VAR, mode)
344}
345
346#[property(CONTEXT, default(JUSTIFY_MODE_VAR), widget_impl(TextAlignMix<P>))]
355pub fn justify_mode(child: impl IntoUiNode, mode: impl IntoVar<Justify>) -> UiNode {
356 with_context_var(child, JUSTIFY_MODE_VAR, mode)
357}
358
359#[widget_mixin]
365pub struct TextWrapMix<P>(P);
366
367context_var! {
368 pub static TEXT_WRAP_VAR: bool = true;
374
375 pub static WORD_BREAK_VAR: WordBreak = WordBreak::Normal;
377
378 pub static LINE_BREAK_VAR: LineBreak = LineBreak::Auto;
380
381 pub static HYPHENS_VAR: Hyphens = Hyphens::default();
383
384 pub static HYPHEN_CHAR_VAR: Txt = Txt::from_char('-');
386
387 pub static TEXT_OVERFLOW_VAR: TextOverflow = TextOverflow::Ignore;
389}
390
391impl TextWrapMix<()> {
392 pub fn context_vars_set(set: &mut ContextValueSet) {
394 set.insert(&TEXT_WRAP_VAR);
395 set.insert(&WORD_BREAK_VAR);
396 set.insert(&LINE_BREAK_VAR);
397 set.insert(&HYPHENS_VAR);
398 set.insert(&HYPHEN_CHAR_VAR);
399 set.insert(&TEXT_OVERFLOW_VAR);
400 }
401}
402
403#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
405pub enum TextOverflow {
406 Ignore,
414 Truncate(Txt),
419}
420impl fmt::Debug for TextOverflow {
421 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 if f.alternate() {
423 write!(f, "TextOverflow")?
424 }
425 match self {
426 Self::Ignore => write!(f, "Ignore"),
427 Self::Truncate(arg0) => f.debug_tuple("Truncate").field(arg0).finish(),
428 }
429 }
430}
431impl TextOverflow {
432 pub fn truncate() -> Self {
434 Self::Truncate(Txt::from_static(""))
435 }
436
437 pub fn ellipses() -> Self {
439 Self::Truncate(Txt::from_char('…'))
440 }
441}
442impl_from_and_into_var! {
443 fn from(truncate: bool) -> TextOverflow {
445 if truncate { TextOverflow::truncate() } else { TextOverflow::Ignore }
446 }
447
448 fn from(truncate: Txt) -> TextOverflow {
449 TextOverflow::Truncate(truncate)
450 }
451 fn from(s: &'static str) -> TextOverflow {
452 Txt::from(s).into()
453 }
454 fn from(s: String) -> TextOverflow {
455 Txt::from(s).into()
456 }
457 fn from(c: char) -> TextOverflow {
458 Txt::from(c).into()
459 }
460}
461
462#[property(CONTEXT, default(TEXT_WRAP_VAR), widget_impl(TextWrapMix<P>))]
473pub fn txt_wrap(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
474 with_context_var(child, TEXT_WRAP_VAR, enabled)
475}
476
477#[property(CONTEXT, default(WORD_BREAK_VAR), widget_impl(TextWrapMix<P>))]
487pub fn word_break(child: impl IntoUiNode, mode: impl IntoVar<WordBreak>) -> UiNode {
488 with_context_var(child, WORD_BREAK_VAR, mode)
489}
490
491#[property(CONTEXT, default(LINE_BREAK_VAR), widget_impl(TextWrapMix<P>))]
495pub fn line_break(child: impl IntoUiNode, mode: impl IntoVar<LineBreak>) -> UiNode {
496 with_context_var(child, LINE_BREAK_VAR, mode)
497}
498
499#[property(CONTEXT, default(HYPHENS_VAR), widget_impl(TextWrapMix<P>))]
509pub fn hyphens(child: impl IntoUiNode, hyphens: impl IntoVar<Hyphens>) -> UiNode {
510 with_context_var(child, HYPHENS_VAR, hyphens)
511}
512
513#[property(CONTEXT, default(HYPHEN_CHAR_VAR), widget_impl(TextWrapMix<P>))]
519pub fn hyphen_char(child: impl IntoUiNode, hyphen: impl IntoVar<Txt>) -> UiNode {
520 with_context_var(child, HYPHEN_CHAR_VAR, hyphen)
521}
522
523#[property(CONTEXT, default(TEXT_OVERFLOW_VAR), widget_impl(TextWrapMix<P>))]
531pub fn txt_overflow(child: impl IntoUiNode, overflow: impl IntoVar<TextOverflow>) -> UiNode {
532 with_context_var(child, TEXT_OVERFLOW_VAR, overflow)
533}
534
535#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
537pub fn is_overflown(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
538 let state = state.into_var();
539 match_node(child, move |_, op| match op {
540 UiNodeOp::Deinit => {
541 state.set(false);
542 }
543 UiNodeOp::Layout { .. } => {
544 let is_o = super::node::TEXT.laidout().overflow.is_some();
545 if is_o != state.get() {
546 state.set(is_o);
547 }
548 }
549 _ => {}
550 })
551}
552
553#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
558pub fn is_line_overflown(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
559 let state = state.into_var();
560 match_node(child, move |_, op| match op {
561 UiNodeOp::Deinit => {
562 state.set(false);
563 }
564 UiNodeOp::Layout { .. } => {
565 let txt = super::node::TEXT.laidout();
566 let is_o = if let Some(info) = &txt.overflow {
567 info.line < txt.shaped_text.lines_len().saturating_sub(1)
568 } else {
569 false
570 };
571 if is_o != state.get() {
572 state.set(is_o);
573 }
574 }
575 _ => {}
576 })
577}
578
579#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
585pub fn get_overflow(child: impl IntoUiNode, txt: impl IntoVar<Txt>) -> UiNode {
586 let txt = txt.into_var();
587 match_node(child, move |_, op| {
588 if let UiNodeOp::Layout { .. } = op {
589 let l_txt = super::node::TEXT.laidout();
590 if let Some(info) = &l_txt.overflow {
591 let r = super::node::TEXT.resolved();
592 let tail = &r.segmented_text.text()[info.text_char..];
593 if txt.with(|t| t != tail) {
594 txt.set(Txt::from_str(tail));
595 }
596 } else if txt.with(|t| !t.is_empty()) {
597 txt.set(Txt::from_static(""));
598 }
599 }
600 })
601}
602
603#[widget_mixin]
609pub struct TextDecorationMix<P>(P);
610
611bitflags! {
612 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
614 #[serde(transparent)]
615 pub struct UnderlineSkip: u8 {
616 const NONE = 0;
618
619 const SPACES = 0b0001;
621
622 const GLYPHS = 0b0010;
624
625 const DEFAULT = Self::GLYPHS.bits();
627 }
628}
629impl Default for UnderlineSkip {
630 fn default() -> Self {
631 Self::DEFAULT
632 }
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
637pub enum UnderlinePosition {
638 #[default]
640 Font,
641 Descent,
643}
644
645context_var! {
646 pub static UNDERLINE_THICKNESS_VAR: UnderlineThickness = 0;
648 pub static UNDERLINE_STYLE_VAR: LineStyle = LineStyle::Hidden;
650 pub static UNDERLINE_COLOR_VAR: Rgba = FONT_COLOR_VAR;
652 pub static UNDERLINE_SKIP_VAR: UnderlineSkip = UnderlineSkip::DEFAULT;
654 pub static UNDERLINE_POSITION_VAR: UnderlinePosition = UnderlinePosition::Font;
656
657 pub static OVERLINE_THICKNESS_VAR: TextLineThickness = 0;
659 pub static OVERLINE_STYLE_VAR: LineStyle = LineStyle::Hidden;
661 pub static OVERLINE_COLOR_VAR: Rgba = FONT_COLOR_VAR;
663
664 pub static STRIKETHROUGH_THICKNESS_VAR: TextLineThickness = 0;
666 pub static STRIKETHROUGH_STYLE_VAR: LineStyle = LineStyle::Hidden;
668 pub static STRIKETHROUGH_COLOR_VAR: Rgba = FONT_COLOR_VAR;
670
671 pub static IME_UNDERLINE_THICKNESS_VAR: UnderlineThickness = 1;
673 pub static IME_UNDERLINE_STYLE_VAR: LineStyle = LineStyle::Dotted;
675}
676
677impl TextDecorationMix<()> {
678 pub fn context_vars_set(set: &mut ContextValueSet) {
680 set.insert(&UNDERLINE_THICKNESS_VAR);
681 set.insert(&UNDERLINE_STYLE_VAR);
682 set.insert(&UNDERLINE_COLOR_VAR);
683 set.insert(&UNDERLINE_SKIP_VAR);
684 set.insert(&UNDERLINE_POSITION_VAR);
685 set.insert(&OVERLINE_THICKNESS_VAR);
686 set.insert(&OVERLINE_STYLE_VAR);
687 set.insert(&OVERLINE_COLOR_VAR);
688 set.insert(&STRIKETHROUGH_THICKNESS_VAR);
689 set.insert(&STRIKETHROUGH_STYLE_VAR);
690 set.insert(&STRIKETHROUGH_COLOR_VAR);
691 set.insert(&IME_UNDERLINE_THICKNESS_VAR);
692 set.insert(&IME_UNDERLINE_STYLE_VAR);
693 }
694}
695
696#[property(CONTEXT, default(UNDERLINE_THICKNESS_VAR, UNDERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
700pub fn underline(child: impl IntoUiNode, thickness: impl IntoVar<UnderlineThickness>, style: impl IntoVar<LineStyle>) -> UiNode {
701 let child = with_context_var(child, UNDERLINE_THICKNESS_VAR, thickness);
702 with_context_var(child, UNDERLINE_STYLE_VAR, style)
703}
704#[property(CONTEXT, default(UNDERLINE_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
709pub fn underline_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
710 with_context_var(child, UNDERLINE_COLOR_VAR, color)
711}
712#[property(CONTEXT, default(UNDERLINE_SKIP_VAR), widget_impl(TextDecorationMix<P>))]
718pub fn underline_skip(child: impl IntoUiNode, skip: impl IntoVar<UnderlineSkip>) -> UiNode {
719 with_context_var(child, UNDERLINE_SKIP_VAR, skip)
720}
721#[property(CONTEXT, default(UNDERLINE_POSITION_VAR), widget_impl(TextDecorationMix<P>))]
728pub fn underline_position(child: impl IntoUiNode, position: impl IntoVar<UnderlinePosition>) -> UiNode {
729 with_context_var(child, UNDERLINE_POSITION_VAR, position)
730}
731
732#[property(CONTEXT, default(OVERLINE_THICKNESS_VAR, OVERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
736pub fn overline(child: impl IntoUiNode, thickness: impl IntoVar<TextLineThickness>, style: impl IntoVar<LineStyle>) -> UiNode {
737 let child = with_context_var(child, OVERLINE_THICKNESS_VAR, thickness);
738 with_context_var(child, OVERLINE_STYLE_VAR, style)
739}
740#[property(CONTEXT, default(OVERLINE_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
745pub fn overline_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
746 with_context_var(child, OVERLINE_COLOR_VAR, color)
747}
748
749#[property(CONTEXT, default(STRIKETHROUGH_THICKNESS_VAR, STRIKETHROUGH_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
753pub fn strikethrough(child: impl IntoUiNode, thickness: impl IntoVar<TextLineThickness>, style: impl IntoVar<LineStyle>) -> UiNode {
754 let child = with_context_var(child, STRIKETHROUGH_THICKNESS_VAR, thickness);
755 with_context_var(child, STRIKETHROUGH_STYLE_VAR, style)
756}
757#[property(CONTEXT, default(STRIKETHROUGH_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
762pub fn strikethrough_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
763 with_context_var(child, STRIKETHROUGH_COLOR_VAR, color)
764}
765
766#[property(CONTEXT, default(IME_UNDERLINE_THICKNESS_VAR, IME_UNDERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
770pub fn ime_underline(child: impl IntoUiNode, thickness: impl IntoVar<UnderlineThickness>, style: impl IntoVar<LineStyle>) -> UiNode {
771 let child = with_context_var(child, IME_UNDERLINE_THICKNESS_VAR, thickness);
772 with_context_var(child, IME_UNDERLINE_STYLE_VAR, style)
773}
774
775#[widget_mixin]
783pub struct TextSpacingMix<P>(P);
784
785context_var! {
786 pub static LINE_HEIGHT_VAR: LineHeight = LineHeight::Default;
790
791 pub static LINE_SPACING_VAR: LineSpacing = LineSpacing::Default;
795
796 pub static LETTER_SPACING_VAR: LetterSpacing = LetterSpacing::Default;
800
801 pub static WORD_SPACING_VAR: WordSpacing = WordSpacing::Default;
805
806 pub static TAB_LENGTH_VAR: TabLength = 400.pct();
808}
809
810impl TextSpacingMix<()> {
811 pub fn context_vars_set(set: &mut ContextValueSet) {
813 set.insert(&LINE_HEIGHT_VAR);
814 set.insert(&LINE_SPACING_VAR);
815 set.insert(&LETTER_SPACING_VAR);
816 set.insert(&WORD_SPACING_VAR);
817 set.insert(&TAB_LENGTH_VAR);
818 set.insert(&PARAGRAPH_INDENT_VAR);
819 }
820}
821
822#[property(CONTEXT, default(LINE_HEIGHT_VAR), widget_impl(TextSpacingMix<P>))]
834pub fn line_height(child: impl IntoUiNode, height: impl IntoVar<LineHeight>) -> UiNode {
835 with_context_var(child, LINE_HEIGHT_VAR, height)
836}
837
838#[property(CONTEXT, default(LETTER_SPACING_VAR), widget_impl(TextSpacingMix<P>))]
854pub fn letter_spacing(child: impl IntoUiNode, extra: impl IntoVar<LetterSpacing>) -> UiNode {
855 with_context_var(child, LETTER_SPACING_VAR, extra)
856}
857
858#[property(CONTEXT, default(LINE_SPACING_VAR), widget_impl(TextSpacingMix<P>))]
869pub fn line_spacing(child: impl IntoUiNode, extra: impl IntoVar<LineSpacing>) -> UiNode {
870 with_context_var(child, LINE_SPACING_VAR, extra)
871}
872
873#[property(CONTEXT, default(WORD_SPACING_VAR), widget_impl(TextSpacingMix<P>))]
893pub fn word_spacing(child: impl IntoUiNode, extra: impl IntoVar<WordSpacing>) -> UiNode {
894 with_context_var(child, WORD_SPACING_VAR, extra)
895}
896
897#[property(CONTEXT, default(TAB_LENGTH_VAR), widget_impl(TextSpacingMix<P>))]
903pub fn tab_length(child: impl IntoUiNode, length: impl IntoVar<TabLength>) -> UiNode {
904 with_context_var(child, TAB_LENGTH_VAR, length)
905}
906
907#[widget_mixin]
913pub struct TextTransformMix<P>(P);
914
915context_var! {
916 pub static WHITE_SPACE_VAR: WhiteSpace = WhiteSpace::Preserve;
920
921 pub static TEXT_TRANSFORM_VAR: TextTransformFn = TextTransformFn::None;
925}
926
927impl TextTransformMix<()> {
928 pub fn context_vars_set(set: &mut ContextValueSet) {
930 set.insert(&WHITE_SPACE_VAR);
931 set.insert(&TEXT_TRANSFORM_VAR);
932 }
933}
934
935#[property(CONTEXT, default(WHITE_SPACE_VAR), widget_impl(TextTransformMix<P>))]
947pub fn white_space(child: impl IntoUiNode, transform: impl IntoVar<WhiteSpace>) -> UiNode {
948 with_context_var(child, WHITE_SPACE_VAR, transform)
949}
950
951#[property(CONTEXT, default(TEXT_TRANSFORM_VAR), widget_impl(TextTransformMix<P>))]
959pub fn txt_transform(child: impl IntoUiNode, transform: impl IntoVar<TextTransformFn>) -> UiNode {
960 with_context_var(child, TEXT_TRANSFORM_VAR, transform)
961}
962
963#[widget_mixin]
969pub struct LangMix<P>(P);
970
971#[property(CONTEXT, default(LANG_VAR), widget_impl(LangMix<P>))]
983pub fn lang(child: impl IntoUiNode, lang: impl IntoVar<Langs>) -> UiNode {
984 let lang = lang.into_var();
985 let child = direction(child, lang.map(|l| l.best().direction()));
986 let child = zng_wgt_access::lang(child, lang.map(|l| l.best().clone()));
987 with_context_var(child, LANG_VAR, lang)
988}
989
990#[property(CONTEXT+1, default(DIRECTION_VAR), widget_impl(LangMix<P>))]
1001pub fn direction(child: impl IntoUiNode, direction: impl IntoVar<LayoutDirection>) -> UiNode {
1002 let child = match_node(child, |child, op| match op {
1003 UiNodeOp::Init => {
1004 WIDGET.sub_var_layout(&DIRECTION_VAR);
1005 }
1006 UiNodeOp::Measure { wm, desired_size } => {
1007 *desired_size = LAYOUT.with_direction(DIRECTION_VAR.get(), || child.measure(wm));
1008 }
1009 UiNodeOp::Layout { wl, final_size } => *final_size = LAYOUT.with_direction(DIRECTION_VAR.get(), || child.layout(wl)),
1010 _ => {}
1011 });
1012 with_context_var(child, DIRECTION_VAR, direction)
1013}
1014
1015impl LangMix<()> {
1016 pub fn context_vars_set(set: &mut ContextValueSet) {
1018 set.insert(&LANG_VAR);
1019 set.insert(&DIRECTION_VAR);
1020 }
1021}
1022
1023#[widget_mixin]
1029pub struct FontFeaturesMix<P>(P);
1030
1031context_var! {
1032 pub static FONT_FEATURES_VAR: FontFeatures = FontFeatures::new();
1036
1037 pub static FONT_VARIATIONS_VAR: FontVariations = FontVariations::new();
1041}
1042
1043impl FontFeaturesMix<()> {
1044 pub fn context_vars_set(set: &mut ContextValueSet) {
1046 set.insert(&FONT_FEATURES_VAR);
1047 set.insert(&FONT_VARIATIONS_VAR);
1048 }
1049}
1050
1051pub fn with_font_variation(child: impl IntoUiNode, name: FontVariationName, value: impl IntoVar<f32>) -> UiNode {
1056 with_context_var(
1057 child,
1058 FONT_VARIATIONS_VAR,
1059 merge_var!(FONT_VARIATIONS_VAR, value.into_var(), move |variations, value| {
1060 let mut variations = variations.clone();
1061 variations.insert(name, *value);
1062 variations
1063 }),
1064 )
1065}
1066
1067pub fn with_font_feature<C, S, V, D>(child: C, state: V, set_feature: D) -> UiNode
1072where
1073 C: IntoUiNode,
1074 S: VarValue,
1075 V: IntoVar<S>,
1076 D: FnMut(&mut FontFeatures, S) -> S + Send + 'static,
1077{
1078 let mut set_feature = set_feature;
1079 with_context_var(
1080 child,
1081 FONT_FEATURES_VAR,
1082 merge_var!(FONT_FEATURES_VAR, state.into_var(), move |features, state| {
1083 let mut features = features.clone();
1084 set_feature(&mut features, state.clone());
1085 features
1086 }),
1087 )
1088}
1089
1090#[property(CONTEXT, default(FONT_VARIATIONS_VAR), widget_impl(FontFeaturesMix<P>))]
1095pub fn font_variations(child: impl IntoUiNode, variations: impl IntoVar<FontVariations>) -> UiNode {
1096 with_context_var(child, FONT_VARIATIONS_VAR, variations)
1097}
1098
1099#[property(CONTEXT, default(FONT_FEATURES_VAR), widget_impl(FontFeaturesMix<P>))]
1104pub fn font_features(child: impl IntoUiNode, features: impl IntoVar<FontFeatures>) -> UiNode {
1105 with_context_var(child, FONT_FEATURES_VAR, features)
1106}
1107
1108#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1110pub fn font_kerning(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1111 with_font_feature(child, state, |f, s| f.kerning().set(s))
1112}
1113
1114#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1116pub fn font_common_lig(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1117 with_font_feature(child, state, |f, s| f.common_lig().set(s))
1118}
1119
1120#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1122pub fn font_discretionary_lig(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1123 with_font_feature(child, state, |f, s| f.discretionary_lig().set(s))
1124}
1125
1126#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1128pub fn font_historical_lig(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1129 with_font_feature(child, state, |f, s| f.historical_lig().set(s))
1130}
1131
1132#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1134pub fn font_contextual_alt(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1135 with_font_feature(child, state, |f, s| f.contextual_alt().set(s))
1136}
1137
1138#[property(CONTEXT, default(CapsVariant::Auto), widget_impl(FontFeaturesMix<P>))]
1140pub fn font_caps(child: impl IntoUiNode, state: impl IntoVar<CapsVariant>) -> UiNode {
1141 with_font_feature(child, state, |f, s| f.caps().set(s))
1142}
1143
1144#[property(CONTEXT, default(NumVariant::Auto), widget_impl(FontFeaturesMix<P>))]
1146pub fn font_numeric(child: impl IntoUiNode, state: impl IntoVar<NumVariant>) -> UiNode {
1147 with_font_feature(child, state, |f, s| f.numeric().set(s))
1148}
1149
1150#[property(CONTEXT, default(NumSpacing::Auto), widget_impl(FontFeaturesMix<P>))]
1152pub fn font_num_spacing(child: impl IntoUiNode, state: impl IntoVar<NumSpacing>) -> UiNode {
1153 with_font_feature(child, state, |f, s| f.num_spacing().set(s))
1154}
1155
1156#[property(CONTEXT, default(NumFraction::Auto), widget_impl(FontFeaturesMix<P>))]
1158pub fn font_num_fraction(child: impl IntoUiNode, state: impl IntoVar<NumFraction>) -> UiNode {
1159 with_font_feature(child, state, |f, s| f.num_fraction().set(s))
1160}
1161
1162#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1164pub fn font_swash(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1165 with_font_feature(child, state, |f, s| f.swash().set(s))
1166}
1167
1168#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1170pub fn font_stylistic(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1171 with_font_feature(child, state, |f, s| f.stylistic().set(s))
1172}
1173
1174#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1176pub fn font_historical_forms(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1177 with_font_feature(child, state, |f, s| f.historical_forms().set(s))
1178}
1179
1180#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1182pub fn font_ornaments(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1183 with_font_feature(child, state, |f, s| f.ornaments().set(s))
1184}
1185
1186#[property(CONTEXT, default(FontFeatureState::auto()), widget_impl(FontFeaturesMix<P>))]
1188pub fn font_annotation(child: impl IntoUiNode, state: impl IntoVar<FontFeatureState>) -> UiNode {
1189 with_font_feature(child, state, |f, s| f.annotation().set(s))
1190}
1191
1192#[property(CONTEXT, default(FontStyleSet::auto()), widget_impl(FontFeaturesMix<P>))]
1194pub fn font_style_set(child: impl IntoUiNode, state: impl IntoVar<FontStyleSet>) -> UiNode {
1195 with_font_feature(child, state, |f, s| f.style_set().set(s))
1196}
1197
1198#[property(CONTEXT, default(CharVariant::auto()), widget_impl(FontFeaturesMix<P>))]
1200pub fn font_char_variant(child: impl IntoUiNode, state: impl IntoVar<CharVariant>) -> UiNode {
1201 with_font_feature(child, state, |f, s| f.char_variant().set(s))
1202}
1203
1204#[property(CONTEXT, default(FontPosition::Auto), widget_impl(FontFeaturesMix<P>))]
1206pub fn font_position(child: impl IntoUiNode, state: impl IntoVar<FontPosition>) -> UiNode {
1207 with_font_feature(child, state, |f, s| f.position().set(s))
1208}
1209
1210#[property(CONTEXT, default(JpVariant::Auto), widget_impl(FontFeaturesMix<P>))]
1212pub fn font_jp_variant(child: impl IntoUiNode, state: impl IntoVar<JpVariant>) -> UiNode {
1213 with_font_feature(child, state, |f, s| f.jp_variant().set(s))
1214}
1215
1216#[property(CONTEXT, default(CnVariant::Auto), widget_impl(FontFeaturesMix<P>))]
1218pub fn font_cn_variant(child: impl IntoUiNode, state: impl IntoVar<CnVariant>) -> UiNode {
1219 with_font_feature(child, state, |f, s| f.cn_variant().set(s))
1220}
1221
1222#[property(CONTEXT, default(EastAsianWidth::Auto), widget_impl(FontFeaturesMix<P>))]
1224pub fn font_ea_width(child: impl IntoUiNode, state: impl IntoVar<EastAsianWidth>) -> UiNode {
1225 with_font_feature(child, state, |f, s| f.ea_width().set(s))
1226}
1227
1228#[widget_mixin]
1234pub struct TextEditMix<P>(P);
1235
1236context_var! {
1237 pub static TEXT_EDITABLE_VAR: bool = false;
1239
1240 pub static TEXT_SELECTABLE_VAR: bool = false;
1242 pub static TEXT_SELECTABLE_ALT_ONLY_VAR: bool = false;
1244
1245 pub static ACCEPTS_TAB_VAR: bool = false;
1247
1248 pub static ACCEPTS_ENTER_VAR: bool = false;
1250
1251 pub static CARET_COLOR_VAR: Rgba = FONT_COLOR_VAR;
1253
1254 pub static INTERACTIVE_CARET_VISUAL_VAR: WidgetFn<CaretShape> = wgt_fn!(|s| super::node::default_interactive_caret_visual(s));
1256
1257 pub static INTERACTIVE_CARET_VAR: InteractiveCaretMode = InteractiveCaretMode::default();
1259
1260 pub static SELECTION_COLOR_VAR: Rgba = colors::AZURE.with_alpha(30.pct());
1262
1263 pub static TXT_PARSE_LIVE_VAR: bool = true;
1265
1266 pub static CHANGE_STOP_DELAY_VAR: Duration = 1.secs();
1268
1269 pub static AUTO_SELECTION_VAR: AutoSelection = AutoSelection::default();
1271
1272 pub static MAX_CHARS_COUNT_VAR: usize = 0;
1276
1277 pub static OBSCURING_CHAR_VAR: char = '•';
1279
1280 pub static OBSCURE_TXT_VAR: bool = false;
1282
1283 pub(super) static TXT_PARSE_PENDING_VAR: bool = false;
1284}
1285
1286impl TextEditMix<()> {
1287 pub fn context_vars_set(set: &mut ContextValueSet) {
1289 set.insert(&TEXT_EDITABLE_VAR);
1290 set.insert(&TEXT_SELECTABLE_VAR);
1291 set.insert(&TEXT_SELECTABLE_ALT_ONLY_VAR);
1292 set.insert(&ACCEPTS_ENTER_VAR);
1293 set.insert(&CARET_COLOR_VAR);
1294 set.insert(&INTERACTIVE_CARET_VISUAL_VAR);
1295 set.insert(&INTERACTIVE_CARET_VAR);
1296 set.insert(&SELECTION_COLOR_VAR);
1297 set.insert(&TXT_PARSE_LIVE_VAR);
1298 set.insert(&CHANGE_STOP_DELAY_VAR);
1299 set.insert(&AUTO_SELECTION_VAR);
1300 set.insert(&MAX_CHARS_COUNT_VAR);
1301 set.insert(&OBSCURING_CHAR_VAR);
1302 set.insert(&OBSCURE_TXT_VAR);
1303 }
1304}
1305
1306#[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1310pub enum CaretShape {
1311 SelectionLeft,
1313 SelectionRight,
1315 Insert,
1317}
1318impl fmt::Debug for CaretShape {
1319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1320 if f.alternate() {
1321 write!(f, "CaretShape::")?;
1322 }
1323 match self {
1324 Self::SelectionLeft => write!(f, "SelectionLeft"),
1325 Self::SelectionRight => write!(f, "SelectionRight"),
1326 Self::Insert => write!(f, "Insert"),
1327 }
1328 }
1329}
1330
1331#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1333pub enum InteractiveCaretMode {
1334 #[default]
1336 TouchOnly,
1337 Enabled,
1339 Disabled,
1341}
1342impl fmt::Debug for InteractiveCaretMode {
1343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1344 if f.alternate() {
1345 write!(f, "InteractiveCaretMode::")?;
1346 }
1347 match self {
1348 Self::TouchOnly => write!(f, "TouchOnly"),
1349 Self::Enabled => write!(f, "Enabled"),
1350 Self::Disabled => write!(f, "Disabled"),
1351 }
1352 }
1353}
1354impl_from_and_into_var! {
1355 fn from(enabled: bool) -> InteractiveCaretMode {
1356 if enabled {
1357 InteractiveCaretMode::Enabled
1358 } else {
1359 InteractiveCaretMode::Disabled
1360 }
1361 }
1362}
1363
1364#[property(CONTEXT, default(TEXT_EDITABLE_VAR), widget_impl(TextEditMix<P>))]
1371pub fn txt_editable(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1372 with_context_var(child, TEXT_EDITABLE_VAR, enabled)
1373}
1374
1375#[property(CONTEXT, default(TEXT_SELECTABLE_VAR), widget_impl(TextEditMix<P>))]
1384pub fn txt_selectable(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1385 with_context_var(child, TEXT_SELECTABLE_VAR, enabled)
1386}
1387
1388#[property(CONTEXT, default(TEXT_SELECTABLE_ALT_ONLY_VAR), widget_impl(TextEditMix<P>))]
1399pub fn txt_selectable_alt_only(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1400 with_context_var(child, TEXT_SELECTABLE_ALT_ONLY_VAR, enabled)
1401}
1402
1403#[property(CONTEXT, default(ACCEPTS_TAB_VAR), widget_impl(TextEditMix<P>))]
1409pub fn accepts_tab(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1410 with_context_var(child, ACCEPTS_TAB_VAR, enabled)
1411}
1412
1413#[property(CONTEXT, default(ACCEPTS_ENTER_VAR), widget_impl(TextEditMix<P>))]
1417pub fn accepts_enter(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1418 with_context_var(child, ACCEPTS_ENTER_VAR, enabled)
1419}
1420
1421#[property(CONTEXT, default(CARET_COLOR_VAR), widget_impl(TextEditMix<P>))]
1425pub fn caret_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
1426 with_context_var(child, CARET_COLOR_VAR, color)
1427}
1428
1429#[property(CONTEXT, default(INTERACTIVE_CARET_VISUAL_VAR), widget_impl(TextEditMix<P>))]
1442pub fn interactive_caret_visual(child: impl IntoUiNode, visual: impl IntoVar<WidgetFn<CaretShape>>) -> UiNode {
1443 with_context_var(child, INTERACTIVE_CARET_VISUAL_VAR, visual)
1444}
1445
1446#[property(CONTEXT, default(INTERACTIVE_CARET_VAR), widget_impl(TextEditMix<P>))]
1450pub fn interactive_caret(child: impl IntoUiNode, mode: impl IntoVar<InteractiveCaretMode>) -> UiNode {
1451 with_context_var(child, INTERACTIVE_CARET_VAR, mode)
1452}
1453
1454#[property(CONTEXT, default(SELECTION_COLOR_VAR), widget_impl(TextEditMix<P>))]
1456pub fn selection_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
1457 with_context_var(child, SELECTION_COLOR_VAR, color)
1458}
1459
1460#[property(CONTEXT, default(TXT_PARSE_LIVE_VAR), widget_impl(TextEditMix<P>))]
1469pub fn txt_parse_live(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1470 with_context_var(child, TXT_PARSE_LIVE_VAR, enabled)
1471}
1472
1473#[property(EVENT, widget_impl(TextEditMix<P>))]
1483pub fn txt_parse_on_stop(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1484 let enabled = enabled.into_var();
1485 let child = txt_parse_live(child, enabled.map(|&b| !b));
1486 on_change_stop(
1487 child,
1488 hn!(|_| {
1489 if enabled.get() {
1490 super::cmd::PARSE_CMD.scoped(WIDGET.id()).notify();
1491 }
1492 }),
1493 )
1494}
1495
1496#[property(CONTEXT, default(MAX_CHARS_COUNT_VAR), widget_impl(TextEditMix<P>))]
1502pub fn max_chars_count(child: impl IntoUiNode, max: impl IntoVar<usize>) -> UiNode {
1503 with_context_var(child, MAX_CHARS_COUNT_VAR, max)
1504}
1505
1506#[property(CONTEXT, default(false), widget_impl(TextEditMix<P>))]
1513pub fn is_parse_pending(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
1514 with_context_var(child, TXT_PARSE_PENDING_VAR, state)
1516}
1517
1518#[property(EVENT, widget_impl(TextEditMix<P>))]
1527pub fn on_change_stop(child: impl IntoUiNode, handler: Handler<ChangeStopArgs>) -> UiNode {
1528 super::node::on_change_stop(child, handler)
1529}
1530
1531#[property(CONTEXT, default(CHANGE_STOP_DELAY_VAR), widget_impl(TextEditMix<P>))]
1542pub fn change_stop_delay(child: impl IntoUiNode, delay: impl IntoVar<Duration>) -> UiNode {
1543 with_context_var(child, CHANGE_STOP_DELAY_VAR, delay)
1544}
1545
1546#[property(CONTEXT, default(AUTO_SELECTION_VAR), widget_impl(TextEditMix<P>))]
1552pub fn auto_selection(child: impl IntoUiNode, mode: impl IntoVar<AutoSelection>) -> UiNode {
1553 with_context_var(child, AUTO_SELECTION_VAR, mode)
1554}
1555
1556#[property(CONTEXT, default(OBSCURING_CHAR_VAR), widget_impl(TextEditMix<P>))]
1562pub fn obscuring_char(child: impl IntoUiNode, character: impl IntoVar<char>) -> UiNode {
1563 with_context_var(child, OBSCURING_CHAR_VAR, character)
1564}
1565
1566#[property(CONTEXT, default(OBSCURE_TXT_VAR), widget_impl(TextEditMix<P>))]
1577pub fn obscure_txt(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1578 with_context_var(child, OBSCURE_TXT_VAR, enabled)
1579}
1580
1581bitflags! {
1582 #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
1586 pub struct AutoSelection: u8 {
1587 const DISABLED = 0;
1589 const CLEAR_ON_BLUR = 0b0000_0001;
1593 const ALL_ON_FOCUS_KEYBOARD = 0b0000_0010;
1595 const ALL_ON_FOCUS_POINTER = 0b0000_0100;
1598 const ENABLED = 0b0000_0111;
1600 }
1601}
1602impl_from_and_into_var! {
1603 fn from(enabled: bool) -> AutoSelection {
1604 if enabled { AutoSelection::ENABLED } else { AutoSelection::DISABLED }
1605 }
1606}
1607impl Default for AutoSelection {
1608 fn default() -> Self {
1609 Self::CLEAR_ON_BLUR
1610 }
1611}
1612
1613#[derive(Debug, Clone)]
1617#[non_exhaustive]
1618pub struct ChangeStopArgs {
1619 pub cause: ChangeStopCause,
1621}
1622impl ChangeStopArgs {
1623 pub fn new(cause: ChangeStopCause) -> Self {
1625 Self { cause }
1626 }
1627}
1628
1629#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1633pub enum ChangeStopCause {
1634 DelayElapsed,
1638 Enter,
1643 Blur,
1645}
1646
1647#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)]
1649pub struct CaretStatus {
1650 index: usize,
1651 line: u32,
1652 column: u32,
1653}
1654impl CaretStatus {
1655 pub fn none() -> Self {
1657 Self {
1658 index: usize::MAX,
1659 line: 0,
1660 column: 0,
1661 }
1662 }
1663
1664 pub fn new(index: usize, text: &SegmentedText) -> Self {
1670 assert!(index <= text.text().len());
1671
1672 if text.text().is_empty() {
1673 Self { line: 1, column: 1, index }
1674 } else {
1675 let mut line = 1;
1676 let mut line_start = 0;
1677 for seg in text.segs() {
1678 if seg.end > index {
1679 break;
1680 }
1681 if let TextSegmentKind::LineBreak = seg.kind {
1682 line += 1;
1683 line_start = seg.end;
1684 }
1685 }
1686
1687 let column = text.text()[line_start..index].chars().count() + 1;
1688
1689 Self {
1690 line,
1691 column: column as _,
1692 index,
1693 }
1694 }
1695 }
1696
1697 pub fn index(&self) -> Option<usize> {
1699 match self.index {
1700 usize::MAX => None,
1701 i => Some(i),
1702 }
1703 }
1704
1705 pub fn line(&self) -> Option<NonZeroU32> {
1709 NonZeroU32::new(self.line)
1710 }
1711
1712 pub fn column(&self) -> Option<NonZeroU32> {
1716 NonZeroU32::new(self.column)
1717 }
1718}
1719impl fmt::Display for CaretStatus {
1720 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1721 if self.index().is_some() {
1722 write!(f, "Ln {}, Col {}", self.line, self.column)
1723 } else {
1724 Ok(())
1725 }
1726 }
1727}
1728
1729#[derive(Debug, Clone, PartialEq, Eq)]
1731pub enum LinesWrapCount {
1732 NoWrap(usize),
1736 Wrap(Vec<u32>),
1740}
1741impl LinesWrapCount {
1742 pub fn lines_len(&self) -> usize {
1744 match self {
1745 Self::NoWrap(l) => *l,
1746 Self::Wrap(lns) => lns.len(),
1747 }
1748 }
1749}
1750
1751#[widget_mixin]
1758pub struct ParagraphMix<P>(P);
1759
1760context_var! {
1761 pub static PARAGRAPH_BREAK_VAR: ParagraphBreak = ParagraphBreak::default();
1765
1766 pub static PARAGRAPH_SPACING_VAR: ParagraphSpacing = 1.em();
1768
1769 pub static PARAGRAPH_INDENT_VAR: Indentation = Indentation::default();
1771}
1772
1773impl ParagraphMix<()> {
1774 pub fn context_vars_set(set: &mut ContextValueSet) {
1776 set.insert(&PARAGRAPH_BREAK_VAR);
1777 set.insert(&PARAGRAPH_SPACING_VAR);
1778 set.insert(&PARAGRAPH_INDENT_VAR);
1779 }
1780}
1781
1782#[property(CONTEXT, default(PARAGRAPH_BREAK_VAR), widget_impl(ParagraphMix<P>))]
1791pub fn paragraph_break(child: impl IntoUiNode, mode: impl IntoVar<ParagraphBreak>) -> UiNode {
1792 with_context_var(child, PARAGRAPH_BREAK_VAR, mode)
1793}
1794
1795#[property(CONTEXT, default(PARAGRAPH_SPACING_VAR), widget_impl(ParagraphMix<P>))]
1805pub fn paragraph_spacing(child: impl IntoUiNode, extra: impl IntoVar<ParagraphSpacing>) -> UiNode {
1806 with_context_var(child, PARAGRAPH_SPACING_VAR, extra)
1807}
1808
1809#[property(CONTEXT, default(PARAGRAPH_INDENT_VAR), widget_impl(ParagraphMix<P>))]
1820pub fn paragraph_indent(child: impl IntoUiNode, indent: impl IntoVar<Indentation>) -> UiNode {
1821 with_context_var(child, PARAGRAPH_INDENT_VAR, indent)
1822}
1823
1824#[widget_mixin]
1826pub struct SelectionToolbarMix<P>(P);
1827
1828context_var! {
1829 pub static SELECTION_TOOLBAR_FN_VAR: WidgetFn<SelectionToolbarArgs> = WidgetFn::nil();
1831 pub static SELECTION_TOOLBAR_ANCHOR_VAR: AnchorOffset = AnchorOffset::out_top();
1833}
1834
1835impl SelectionToolbarMix<()> {
1836 pub fn context_vars_set(set: &mut ContextValueSet) {
1838 set.insert(&SELECTION_TOOLBAR_FN_VAR);
1839 set.insert(&SELECTION_TOOLBAR_ANCHOR_VAR);
1840 }
1841}
1842
1843#[property(CONTEXT, widget_impl(SelectionToolbarMix<P>))]
1847pub fn selection_toolbar(child: impl IntoUiNode, toolbar: impl IntoUiNode) -> UiNode {
1848 selection_toolbar_fn(child, WidgetFn::singleton(toolbar))
1849}
1850
1851#[property(CONTEXT, default(SELECTION_TOOLBAR_FN_VAR), widget_impl(SelectionToolbarMix<P>))]
1855pub fn selection_toolbar_fn(child: impl IntoUiNode, toolbar: impl IntoVar<WidgetFn<SelectionToolbarArgs>>) -> UiNode {
1856 with_context_var(child, SELECTION_TOOLBAR_FN_VAR, toolbar)
1857}
1858
1859#[non_exhaustive]
1863pub struct SelectionToolbarArgs {
1864 pub anchor_id: WidgetId,
1866 pub is_touch: bool,
1868}
1869impl SelectionToolbarArgs {
1870 pub fn new(anchor_id: impl Into<WidgetId>, is_touch: bool) -> Self {
1872 Self {
1873 anchor_id: anchor_id.into(),
1874 is_touch,
1875 }
1876 }
1877}
1878
1879#[property(CONTEXT, default(SELECTION_TOOLBAR_ANCHOR_VAR), widget_impl(SelectionToolbarMix<P>))]
1885pub fn selection_toolbar_anchor(child: impl IntoUiNode, offset: impl IntoVar<AnchorOffset>) -> UiNode {
1886 with_context_var(child, SELECTION_TOOLBAR_ANCHOR_VAR, offset)
1887}
1888
1889#[widget_mixin]
1891pub struct TextInspectMix<P>(P);
1892
1893impl TextInspectMix<()> {
1894 pub fn context_vars_set(set: &mut ContextValueSet) {
1896 let _ = set;
1897 }
1898}
1899
1900#[property(EVENT, default(None), widget_impl(TextInspectMix<P>))]
1902pub fn get_caret_index(child: impl IntoUiNode, index: impl IntoVar<Option<CaretIndex>>) -> UiNode {
1903 super::node::get_caret_index(child, index)
1904}
1905
1906#[property(EVENT, default(CaretStatus::none()), widget_impl(TextInspectMix<P>))]
1908pub fn get_caret_status(child: impl IntoUiNode, status: impl IntoVar<CaretStatus>) -> UiNode {
1909 super::node::get_caret_status(child, status)
1910}
1911
1912#[property(CHILD_LAYOUT+100, default(0), widget_impl(TextInspectMix<P>))]
1919pub fn get_lines_len(child: impl IntoUiNode, len: impl IntoVar<usize>) -> UiNode {
1920 super::node::get_lines_len(child, len)
1921}
1922
1923#[property(CHILD_LAYOUT+100, default(LinesWrapCount::NoWrap(0)), widget_impl(TextInspectMix<P>))]
1925pub fn get_lines_wrap_count(child: impl IntoUiNode, lines: impl IntoVar<LinesWrapCount>) -> UiNode {
1926 super::node::get_lines_wrap_count(child, lines)
1927}
1928
1929#[property(EVENT, default(0), widget_impl(TextInspectMix<P>))]
1931pub fn get_chars_count(child: impl IntoUiNode, chars: impl IntoVar<usize>) -> UiNode {
1932 let chars = chars.into_var();
1933 match_node(child, move |_, op| {
1934 if let UiNodeOp::Init = op {
1935 let ctx = super::node::TEXT.resolved();
1936 chars.set_from_map(&ctx.txt, |t| t.chars().count());
1937 let handle = ctx.txt.bind_map(&chars, |t| t.chars().count());
1938 WIDGET.push_var_handle(handle);
1939 }
1940 })
1941}
1942
1943#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
1947pub fn txt_highlight(child: impl IntoUiNode, range: impl IntoVar<std::ops::Range<CaretIndex>>, color: impl IntoVar<Rgba>) -> UiNode {
1948 let range = range.into_var();
1949 let color = color.into_var();
1950 let color_key = FrameValueKey::new_unique();
1951 match_node(child, move |_, op| match op {
1952 UiNodeOp::Init => {
1953 WIDGET.sub_var_render(&range).sub_var_render_update(&color);
1954 }
1955 UiNodeOp::Render { frame } => {
1956 let l_txt = super::node::TEXT.laidout();
1957 let r_txt = super::node::TEXT.resolved();
1958 let r_txt = r_txt.segmented_text.text();
1959
1960 for line_rect in l_txt.shaped_text.highlight_rects(range.get(), r_txt) {
1961 frame.push_color(line_rect, color_key.bind_var(&color, |c| *c));
1962 }
1963 }
1964 UiNodeOp::RenderUpdate { update } => {
1965 if let Some(color_update) = color_key.update_var(&color, |c| *c) {
1966 update.update_color(color_update)
1967 }
1968 }
1969 _ => {}
1970 })
1971}
1972
1973#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
1977pub fn get_font_use(child: impl IntoUiNode, font_use: impl IntoVar<Vec<(Font, std::ops::Range<usize>)>>) -> UiNode {
1978 let font_use = font_use.into_var();
1979 let mut shaped_text_version = u32::MAX;
1980 match_node(child, move |c, op| {
1981 if let UiNodeOp::Layout { wl, final_size } = op {
1982 *final_size = c.layout(wl);
1983
1984 let ctx = crate::node::TEXT.laidout();
1985
1986 if ctx.shaped_text_version != shaped_text_version && font_use.capabilities().can_modify() {
1987 shaped_text_version = ctx.shaped_text_version;
1988
1989 let mut r = vec![];
1990
1991 for seg in ctx.shaped_text.lines().flat_map(|l| l.segs()) {
1992 let mut seg_glyph_i = 0;
1993 let seg_range = seg.text_range();
1994
1995 for (font, glyphs) in seg.glyphs() {
1996 if r.is_empty() {
1997 r.push((font.clone(), 0..seg_range.end));
1998 } else {
1999 let last_i = r.len() - 1;
2000 if &r[last_i].0 != font {
2001 let seg_char_i = seg.clusters()[seg_glyph_i] as usize;
2002
2003 let char_i = seg_range.start + seg_char_i;
2004 r[last_i].1.end = char_i;
2005 r.push((font.clone(), char_i..seg_range.end));
2006 }
2007 }
2008 seg_glyph_i += glyphs.len();
2009 }
2010 }
2011
2012 font_use.set(r);
2013 }
2014 }
2015 })
2016}
2017
2018#[property(EVENT, widget_impl(TextInspectMix<P>))]
2022pub fn has_selection(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
2023 let state = state.into_var();
2024 match_node(child, move |c, op| {
2025 let mut update = false;
2026 match &op {
2027 UiNodeOp::Init => {
2028 update = true;
2029 }
2030 UiNodeOp::Deinit => {
2031 state.set(false);
2032 }
2033 UiNodeOp::Event { .. } => {
2034 update = true;
2035 }
2036 UiNodeOp::Update { .. } => {
2037 update = true;
2038 }
2039 _ => {}
2040 }
2041
2042 c.op(op);
2043
2044 if update {
2045 let new = if let Some(ctx) = TEXT.try_rich() {
2046 ctx.caret.selection_index.is_some()
2047 } else if let Some(ctx) = TEXT.try_resolved() {
2048 ctx.caret.selection_index.is_some()
2049 } else {
2050 false
2051 };
2052 if new != state.get() {
2053 state.set(new);
2054 }
2055 }
2056 })
2057}
2058
2059#[property(EVENT, default(Txt::from_static("")), widget_impl(TextInspectMix<P>))]
2061pub fn get_selection(child: impl IntoUiNode, state: impl IntoVar<Txt>) -> UiNode {
2062 let state = state.into_var();
2063 let mut last_sel = (CaretIndex::ZERO, None::<CaretIndex>);
2064 match_node(child, move |c, op| {
2065 let mut update = false;
2066 match &op {
2067 UiNodeOp::Init => {
2068 update = true;
2069 }
2070 UiNodeOp::Deinit => {
2071 state.set(Txt::from_static(""));
2072 }
2073 UiNodeOp::Event { .. } => {
2074 update = true;
2075 }
2076 UiNodeOp::Update { .. } => {
2077 update = true;
2078 }
2079 _ => {}
2080 }
2081
2082 c.op(op);
2083
2084 if update && let Some(ctx) = TEXT.try_resolved() {
2085 let new_sel = (ctx.caret.index.unwrap_or(CaretIndex::ZERO), ctx.caret.selection_index);
2086
2087 if last_sel != new_sel {
2088 last_sel = new_sel;
2089
2090 let txt = if let Some(range) = ctx.caret.selection_char_range() {
2091 Txt::from_str(&ctx.segmented_text.text()[range])
2092 } else {
2093 Txt::from_static("")
2094 };
2095
2096 state.set(txt);
2097 }
2098 }
2099 })
2100}
2101
2102#[widget_mixin]
2106pub struct RichTextMix<P>(P);
2107
2108context_var! {
2109 pub static RICH_TEXT_FOCUSED_Z_VAR: Option<ZIndex> = ZIndex::FRONT;
2111}
2112
2113impl RichTextMix<()> {
2114 pub fn context_vars_set(set: &mut ContextValueSet) {
2116 set.insert(&RICH_TEXT_FOCUSED_Z_VAR);
2117 }
2118}
2119
2120#[property(CONTEXT, default(false), widget_impl(RichTextMix<P>))]
2131pub fn rich_text(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
2132 crate::node::rich_text_node(child, enabled)
2133}
2134
2135#[property(CONTEXT, default(RICH_TEXT_FOCUSED_Z_VAR), widget_impl(RichTextMix<P>))]
2142pub fn rich_text_focused_z(child: impl IntoUiNode, z_index: impl IntoVar<Option<ZIndex>>) -> UiNode {
2143 with_context_var(child, RICH_TEXT_FOCUSED_Z_VAR, z_index)
2144}