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 = FontName::system_ui();
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>))]
1386pub fn txt_selectable(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1387 with_context_var(child, TEXT_SELECTABLE_VAR, enabled)
1388}
1389
1390#[property(CONTEXT, default(TEXT_SELECTABLE_ALT_ONLY_VAR), widget_impl(TextEditMix<P>))]
1401pub fn txt_selectable_alt_only(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1402 with_context_var(child, TEXT_SELECTABLE_ALT_ONLY_VAR, enabled)
1403}
1404
1405#[property(CONTEXT, default(ACCEPTS_TAB_VAR), widget_impl(TextEditMix<P>))]
1411pub fn accepts_tab(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1412 with_context_var(child, ACCEPTS_TAB_VAR, enabled)
1413}
1414
1415#[property(CONTEXT, default(ACCEPTS_ENTER_VAR), widget_impl(TextEditMix<P>))]
1419pub fn accepts_enter(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1420 with_context_var(child, ACCEPTS_ENTER_VAR, enabled)
1421}
1422
1423#[property(CONTEXT, default(CARET_COLOR_VAR), widget_impl(TextEditMix<P>))]
1427pub fn caret_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
1428 with_context_var(child, CARET_COLOR_VAR, color)
1429}
1430
1431#[property(CONTEXT, default(INTERACTIVE_CARET_VISUAL_VAR), widget_impl(TextEditMix<P>))]
1444pub fn interactive_caret_visual(child: impl IntoUiNode, visual: impl IntoVar<WidgetFn<CaretShape>>) -> UiNode {
1445 with_context_var(child, INTERACTIVE_CARET_VISUAL_VAR, visual)
1446}
1447
1448#[property(CONTEXT, default(INTERACTIVE_CARET_VAR), widget_impl(TextEditMix<P>))]
1452pub fn interactive_caret(child: impl IntoUiNode, mode: impl IntoVar<InteractiveCaretMode>) -> UiNode {
1453 with_context_var(child, INTERACTIVE_CARET_VAR, mode)
1454}
1455
1456#[property(CONTEXT, default(SELECTION_COLOR_VAR), widget_impl(TextEditMix<P>))]
1458pub fn selection_color(child: impl IntoUiNode, color: impl IntoVar<Rgba>) -> UiNode {
1459 with_context_var(child, SELECTION_COLOR_VAR, color)
1460}
1461
1462#[property(CONTEXT, default(TXT_PARSE_LIVE_VAR), widget_impl(TextEditMix<P>))]
1471pub fn txt_parse_live(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1472 with_context_var(child, TXT_PARSE_LIVE_VAR, enabled)
1473}
1474
1475#[property(EVENT, widget_impl(TextEditMix<P>))]
1485pub fn txt_parse_on_stop(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1486 let enabled = enabled.into_var();
1487 let child = txt_parse_live(child, enabled.map(|&b| !b));
1488 on_change_stop(
1489 child,
1490 hn!(|_| {
1491 if enabled.get() {
1492 super::cmd::PARSE_CMD.scoped(WIDGET.id()).notify();
1493 }
1494 }),
1495 )
1496}
1497
1498#[property(CONTEXT, default(MAX_CHARS_COUNT_VAR), widget_impl(TextEditMix<P>))]
1504pub fn max_chars_count(child: impl IntoUiNode, max: impl IntoVar<usize>) -> UiNode {
1505 with_context_var(child, MAX_CHARS_COUNT_VAR, max)
1506}
1507
1508#[property(CONTEXT, default(false), widget_impl(TextEditMix<P>))]
1515pub fn is_parse_pending(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
1516 with_context_var(child, TXT_PARSE_PENDING_VAR, state)
1518}
1519
1520#[property(EVENT, widget_impl(TextEditMix<P>))]
1529pub fn on_change_stop(child: impl IntoUiNode, handler: Handler<ChangeStopArgs>) -> UiNode {
1530 super::node::on_change_stop(child, handler)
1531}
1532
1533#[property(CONTEXT, default(CHANGE_STOP_DELAY_VAR), widget_impl(TextEditMix<P>))]
1544pub fn change_stop_delay(child: impl IntoUiNode, delay: impl IntoVar<Duration>) -> UiNode {
1545 with_context_var(child, CHANGE_STOP_DELAY_VAR, delay)
1546}
1547
1548#[property(CONTEXT, default(AUTO_SELECTION_VAR), widget_impl(TextEditMix<P>))]
1554pub fn auto_selection(child: impl IntoUiNode, mode: impl IntoVar<AutoSelection>) -> UiNode {
1555 with_context_var(child, AUTO_SELECTION_VAR, mode)
1556}
1557
1558#[property(CONTEXT, default(OBSCURING_CHAR_VAR), widget_impl(TextEditMix<P>))]
1564pub fn obscuring_char(child: impl IntoUiNode, character: impl IntoVar<char>) -> UiNode {
1565 with_context_var(child, OBSCURING_CHAR_VAR, character)
1566}
1567
1568#[property(CONTEXT, default(OBSCURE_TXT_VAR), widget_impl(TextEditMix<P>))]
1579pub fn obscure_txt(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
1580 with_context_var(child, OBSCURE_TXT_VAR, enabled)
1581}
1582
1583bitflags! {
1584 #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
1588 pub struct AutoSelection: u8 {
1589 const DISABLED = 0;
1591 const CLEAR_ON_BLUR = 0b0000_0001;
1595 const ALL_ON_FOCUS_KEYBOARD = 0b0000_0010;
1597 const ALL_ON_FOCUS_POINTER = 0b0000_0100;
1600 const ENABLED = 0b0000_0111;
1602 }
1603}
1604impl_from_and_into_var! {
1605 fn from(enabled: bool) -> AutoSelection {
1606 if enabled { AutoSelection::ENABLED } else { AutoSelection::DISABLED }
1607 }
1608}
1609impl Default for AutoSelection {
1610 fn default() -> Self {
1611 Self::CLEAR_ON_BLUR
1612 }
1613}
1614
1615#[derive(Debug, Clone)]
1619#[non_exhaustive]
1620pub struct ChangeStopArgs {
1621 pub cause: ChangeStopCause,
1623}
1624impl ChangeStopArgs {
1625 pub fn new(cause: ChangeStopCause) -> Self {
1627 Self { cause }
1628 }
1629}
1630
1631#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1635pub enum ChangeStopCause {
1636 DelayElapsed,
1640 Enter,
1645 Blur,
1647}
1648
1649#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)]
1651pub struct CaretStatus {
1652 index: usize,
1653 line: u32,
1654 column: u32,
1655}
1656impl CaretStatus {
1657 pub fn none() -> Self {
1659 Self {
1660 index: usize::MAX,
1661 line: 0,
1662 column: 0,
1663 }
1664 }
1665
1666 pub fn new(index: usize, text: &SegmentedText) -> Self {
1672 assert!(index <= text.text().len());
1673
1674 if text.text().is_empty() {
1675 Self { line: 1, column: 1, index }
1676 } else {
1677 let mut line = 1;
1678 let mut line_start = 0;
1679 for seg in text.segs() {
1680 if seg.end > index {
1681 break;
1682 }
1683 if let TextSegmentKind::LineBreak = seg.kind {
1684 line += 1;
1685 line_start = seg.end;
1686 }
1687 }
1688
1689 let column = text.text()[line_start..index].chars().count() + 1;
1690
1691 Self {
1692 line,
1693 column: column as _,
1694 index,
1695 }
1696 }
1697 }
1698
1699 pub fn index(&self) -> Option<usize> {
1701 match self.index {
1702 usize::MAX => None,
1703 i => Some(i),
1704 }
1705 }
1706
1707 pub fn line(&self) -> Option<NonZeroU32> {
1711 NonZeroU32::new(self.line)
1712 }
1713
1714 pub fn column(&self) -> Option<NonZeroU32> {
1718 NonZeroU32::new(self.column)
1719 }
1720}
1721impl fmt::Display for CaretStatus {
1722 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1723 if self.index().is_some() {
1724 write!(f, "Ln {}, Col {}", self.line, self.column)
1725 } else {
1726 Ok(())
1727 }
1728 }
1729}
1730
1731#[derive(Debug, Clone, PartialEq, Eq)]
1733pub enum LinesWrapCount {
1734 NoWrap(usize),
1738 Wrap(Vec<u32>),
1742}
1743impl LinesWrapCount {
1744 pub fn lines_len(&self) -> usize {
1746 match self {
1747 Self::NoWrap(l) => *l,
1748 Self::Wrap(lns) => lns.len(),
1749 }
1750 }
1751}
1752
1753#[widget_mixin]
1760pub struct ParagraphMix<P>(P);
1761
1762context_var! {
1763 pub static PARAGRAPH_BREAK_VAR: ParagraphBreak = ParagraphBreak::default();
1767
1768 pub static PARAGRAPH_SPACING_VAR: ParagraphSpacing = 1.em();
1770
1771 pub static PARAGRAPH_INDENT_VAR: Indentation = Indentation::default();
1773}
1774
1775impl ParagraphMix<()> {
1776 pub fn context_vars_set(set: &mut ContextValueSet) {
1778 set.insert(&PARAGRAPH_BREAK_VAR);
1779 set.insert(&PARAGRAPH_SPACING_VAR);
1780 set.insert(&PARAGRAPH_INDENT_VAR);
1781 }
1782}
1783
1784#[property(CONTEXT, default(PARAGRAPH_BREAK_VAR), widget_impl(ParagraphMix<P>))]
1793pub fn paragraph_break(child: impl IntoUiNode, mode: impl IntoVar<ParagraphBreak>) -> UiNode {
1794 with_context_var(child, PARAGRAPH_BREAK_VAR, mode)
1795}
1796
1797#[property(CONTEXT, default(PARAGRAPH_SPACING_VAR), widget_impl(ParagraphMix<P>))]
1807pub fn paragraph_spacing(child: impl IntoUiNode, extra: impl IntoVar<ParagraphSpacing>) -> UiNode {
1808 with_context_var(child, PARAGRAPH_SPACING_VAR, extra)
1809}
1810
1811#[property(CONTEXT, default(PARAGRAPH_INDENT_VAR), widget_impl(ParagraphMix<P>))]
1822pub fn paragraph_indent(child: impl IntoUiNode, indent: impl IntoVar<Indentation>) -> UiNode {
1823 with_context_var(child, PARAGRAPH_INDENT_VAR, indent)
1824}
1825
1826#[widget_mixin]
1828pub struct SelectionToolbarMix<P>(P);
1829
1830context_var! {
1831 pub static SELECTION_TOOLBAR_FN_VAR: WidgetFn<SelectionToolbarArgs> = WidgetFn::nil();
1833 pub static SELECTION_TOOLBAR_ANCHOR_VAR: AnchorOffset = AnchorOffset::out_top();
1835}
1836
1837impl SelectionToolbarMix<()> {
1838 pub fn context_vars_set(set: &mut ContextValueSet) {
1840 set.insert(&SELECTION_TOOLBAR_FN_VAR);
1841 set.insert(&SELECTION_TOOLBAR_ANCHOR_VAR);
1842 }
1843}
1844
1845#[property(CONTEXT, widget_impl(SelectionToolbarMix<P>))]
1849pub fn selection_toolbar(child: impl IntoUiNode, toolbar: impl IntoUiNode) -> UiNode {
1850 selection_toolbar_fn(child, WidgetFn::singleton(toolbar))
1851}
1852
1853#[property(CONTEXT, default(SELECTION_TOOLBAR_FN_VAR), widget_impl(SelectionToolbarMix<P>))]
1857pub fn selection_toolbar_fn(child: impl IntoUiNode, toolbar: impl IntoVar<WidgetFn<SelectionToolbarArgs>>) -> UiNode {
1858 with_context_var(child, SELECTION_TOOLBAR_FN_VAR, toolbar)
1859}
1860
1861#[non_exhaustive]
1865pub struct SelectionToolbarArgs {
1866 pub anchor_id: WidgetId,
1868 pub is_touch: bool,
1870}
1871impl SelectionToolbarArgs {
1872 pub fn new(anchor_id: impl Into<WidgetId>, is_touch: bool) -> Self {
1874 Self {
1875 anchor_id: anchor_id.into(),
1876 is_touch,
1877 }
1878 }
1879}
1880
1881#[property(CONTEXT, default(SELECTION_TOOLBAR_ANCHOR_VAR), widget_impl(SelectionToolbarMix<P>))]
1887pub fn selection_toolbar_anchor(child: impl IntoUiNode, offset: impl IntoVar<AnchorOffset>) -> UiNode {
1888 with_context_var(child, SELECTION_TOOLBAR_ANCHOR_VAR, offset)
1889}
1890
1891#[widget_mixin]
1893pub struct TextInspectMix<P>(P);
1894
1895impl TextInspectMix<()> {
1896 pub fn context_vars_set(set: &mut ContextValueSet) {
1898 let _ = set;
1899 }
1900}
1901
1902#[property(EVENT, default(None), widget_impl(TextInspectMix<P>))]
1904pub fn get_caret_index(child: impl IntoUiNode, index: impl IntoVar<Option<CaretIndex>>) -> UiNode {
1905 super::node::get_caret_index(child, index)
1906}
1907
1908#[property(EVENT, default(CaretStatus::none()), widget_impl(TextInspectMix<P>))]
1910pub fn get_caret_status(child: impl IntoUiNode, status: impl IntoVar<CaretStatus>) -> UiNode {
1911 super::node::get_caret_status(child, status)
1912}
1913
1914#[property(CHILD_LAYOUT+100, default(0), widget_impl(TextInspectMix<P>))]
1921pub fn get_lines_len(child: impl IntoUiNode, len: impl IntoVar<usize>) -> UiNode {
1922 super::node::get_lines_len(child, len)
1923}
1924
1925#[property(CHILD_LAYOUT+100, default(LinesWrapCount::NoWrap(0)), widget_impl(TextInspectMix<P>))]
1927pub fn get_lines_wrap_count(child: impl IntoUiNode, lines: impl IntoVar<LinesWrapCount>) -> UiNode {
1928 super::node::get_lines_wrap_count(child, lines)
1929}
1930
1931#[property(EVENT, default(0), widget_impl(TextInspectMix<P>))]
1933pub fn get_chars_count(child: impl IntoUiNode, chars: impl IntoVar<usize>) -> UiNode {
1934 let chars = chars.into_var();
1935 match_node(child, move |_, op| {
1936 if let UiNodeOp::Init = op {
1937 let ctx = super::node::TEXT.resolved();
1938 chars.set_from_map(&ctx.txt, |t| t.chars().count());
1939 let handle = ctx.txt.bind_map(&chars, |t| t.chars().count());
1940 WIDGET.push_var_handle(handle);
1941 }
1942 })
1943}
1944
1945#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
1949pub fn txt_highlight(child: impl IntoUiNode, range: impl IntoVar<std::ops::Range<CaretIndex>>, color: impl IntoVar<Rgba>) -> UiNode {
1950 let range = range.into_var();
1951 let color = color.into_var();
1952 let color_key = FrameValueKey::new_unique();
1953 match_node(child, move |_, op| match op {
1954 UiNodeOp::Init => {
1955 WIDGET.sub_var_render(&range).sub_var_render_update(&color);
1956 }
1957 UiNodeOp::Render { frame } => {
1958 let l_txt = super::node::TEXT.laidout();
1959 let r_txt = super::node::TEXT.resolved();
1960 let r_txt = r_txt.segmented_text.text();
1961
1962 for line_rect in l_txt.shaped_text.highlight_rects(range.get(), r_txt) {
1963 frame.push_color(line_rect, color_key.bind_var(&color, |c| *c));
1964 }
1965 }
1966 UiNodeOp::RenderUpdate { update } => {
1967 if let Some(color_update) = color_key.update_var(&color, |c| *c) {
1968 update.update_color(color_update)
1969 }
1970 }
1971 _ => {}
1972 })
1973}
1974
1975#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
1979pub fn get_font_use(child: impl IntoUiNode, font_use: impl IntoVar<Vec<(Font, std::ops::Range<usize>)>>) -> UiNode {
1980 let font_use = font_use.into_var();
1981 let mut shaped_text_version = u32::MAX;
1982 match_node(child, move |c, op| {
1983 if let UiNodeOp::Layout { wl, final_size } = op {
1984 *final_size = c.layout(wl);
1985
1986 let ctx = crate::node::TEXT.laidout();
1987
1988 if ctx.shaped_text_version != shaped_text_version && font_use.capabilities().can_modify() {
1989 shaped_text_version = ctx.shaped_text_version;
1990
1991 let mut r = vec![];
1992
1993 for seg in ctx.shaped_text.lines().flat_map(|l| l.segs()) {
1994 let mut seg_glyph_i = 0;
1995 let seg_range = seg.text_range();
1996
1997 for (font, glyphs) in seg.glyphs() {
1998 if r.is_empty() {
1999 r.push((font.clone(), 0..seg_range.end));
2000 } else {
2001 let last_i = r.len() - 1;
2002 if &r[last_i].0 != font {
2003 let seg_char_i = seg.clusters()[seg_glyph_i] as usize;
2004
2005 let char_i = seg_range.start + seg_char_i;
2006 r[last_i].1.end = char_i;
2007 r.push((font.clone(), char_i..seg_range.end));
2008 }
2009 }
2010 seg_glyph_i += glyphs.len();
2011 }
2012 }
2013
2014 font_use.set(r);
2015 }
2016 }
2017 })
2018}
2019
2020#[property(EVENT, widget_impl(TextInspectMix<P>))]
2024pub fn has_selection(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
2025 let state = state.into_var();
2026 match_node(child, move |c, op| {
2027 let mut update = false;
2028 match &op {
2029 UiNodeOp::Init => {
2030 update = true;
2031 }
2032 UiNodeOp::Deinit => {
2033 state.set(false);
2034 }
2035 UiNodeOp::Update { .. } => {
2036 update = true;
2037 }
2038 _ => {}
2039 }
2040
2041 c.op(op);
2042
2043 if update {
2044 let new = if let Some(ctx) = TEXT.try_rich() {
2045 ctx.caret.selection_index.is_some()
2046 } else if let Some(ctx) = TEXT.try_resolved() {
2047 ctx.caret.selection_index.is_some()
2048 } else {
2049 false
2050 };
2051 if new != state.get() {
2052 state.set(new);
2053 }
2054 }
2055 })
2056}
2057
2058#[property(EVENT, default(Txt::from_static("")), widget_impl(TextInspectMix<P>))]
2060pub fn get_selection(child: impl IntoUiNode, state: impl IntoVar<Txt>) -> UiNode {
2061 let state = state.into_var();
2062 let mut last_sel = (CaretIndex::ZERO, None::<CaretIndex>);
2063 match_node(child, move |c, op| {
2064 let mut update = false;
2065 match &op {
2066 UiNodeOp::Init => {
2067 update = true;
2068 }
2069 UiNodeOp::Deinit => {
2070 state.set(Txt::from_static(""));
2071 }
2072 UiNodeOp::Update { .. } => {
2073 update = true;
2074 }
2075 _ => {}
2076 }
2077
2078 c.op(op);
2079
2080 if update && let Some(ctx) = TEXT.try_resolved() {
2081 let new_sel = (ctx.caret.index.unwrap_or(CaretIndex::ZERO), ctx.caret.selection_index);
2082
2083 if last_sel != new_sel {
2084 last_sel = new_sel;
2085
2086 let txt = if let Some(range) = ctx.caret.selection_char_range() {
2087 Txt::from_str(&ctx.segmented_text.text()[range])
2088 } else {
2089 Txt::from_static("")
2090 };
2091
2092 state.set(txt);
2093 }
2094 }
2095 })
2096}
2097
2098#[widget_mixin]
2102pub struct RichTextMix<P>(P);
2103
2104context_var! {
2105 pub static RICH_TEXT_FOCUSED_Z_VAR: Option<ZIndex> = ZIndex::FRONT;
2107}
2108
2109impl RichTextMix<()> {
2110 pub fn context_vars_set(set: &mut ContextValueSet) {
2112 set.insert(&RICH_TEXT_FOCUSED_Z_VAR);
2113 }
2114}
2115
2116#[property(CONTEXT, default(false), widget_impl(RichTextMix<P>))]
2127pub fn rich_text(wgt: &mut WidgetBuilding, enabled: impl IntoVar<bool>) {
2128 let enabled = enabled.into_var();
2129 wgt.push_intrinsic(NestGroup::CONTEXT, "rich_text", move |c| crate::node::rich_text_node(c, enabled));
2131 wgt.push_intrinsic(NestGroup::EVENT, "rich_text_broadcast", crate::node::rich_text_events_broadcast);
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}