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
11#[widget_mixin]
19pub struct FontMix<P>(P);
20
21context_var! {
22 pub static FONT_FAMILY_VAR: FontNames = FontNames::default();
26
27 pub static FONT_SIZE_VAR: FontSize = FontSize::Pt(11.0);
31
32 pub static FONT_WEIGHT_VAR: FontWeight = FontWeight::NORMAL;
36
37 pub static FONT_STYLE_VAR: FontStyle = FontStyle::Normal;
41
42 pub static FONT_STRETCH_VAR: FontStretch = FontStretch::NORMAL;
46
47 pub static FONT_SYNTHESIS_VAR: FontSynthesis = FontSynthesis::ENABLED;
51
52 pub static FONT_AA_VAR: FontAntiAliasing = FontAntiAliasing::Default;
56}
57
58impl FontMix<()> {
59 pub fn context_vars_set(set: &mut ContextValueSet) {
61 set.insert(&FONT_FAMILY_VAR);
62 set.insert(&FONT_SIZE_VAR);
63 set.insert(&FONT_WEIGHT_VAR);
64 set.insert(&FONT_STYLE_VAR);
65 set.insert(&FONT_STRETCH_VAR);
66 set.insert(&FONT_SYNTHESIS_VAR);
67 set.insert(&FONT_AA_VAR);
68 }
69}
70
71#[property(CONTEXT, default(FONT_FAMILY_VAR), widget_impl(FontMix<P>))]
83pub fn font_family(child: impl UiNode, names: impl IntoVar<FontNames>) -> impl UiNode {
84 with_context_var(child, FONT_FAMILY_VAR, names)
85}
86
87#[property(CONTEXT, default(FONT_SIZE_VAR), widget_impl(FontMix<P>))]
96pub fn font_size(child: impl UiNode, size: impl IntoVar<FontSize>) -> impl UiNode {
97 let child = match_node(child, |child, op| match op {
98 UiNodeOp::Init => {
99 WIDGET.sub_var_layout(&FONT_SIZE_VAR);
100 }
101 UiNodeOp::Measure { wm, desired_size } => {
102 let font_size = FONT_SIZE_VAR.get();
103 let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
104 *desired_size = if font_size_px >= 0 {
105 LAYOUT.with_font_size(font_size_px, || child.measure(wm))
106 } else {
107 tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
108 child.measure(wm)
109 };
110 }
111 UiNodeOp::Layout { wl, final_size } => {
112 let font_size = FONT_SIZE_VAR.get();
113 let font_size_px = font_size.layout_dft_x(LAYOUT.root_font_size());
114 *final_size = if font_size_px >= 0 {
115 LAYOUT.with_font_size(font_size_px, || child.layout(wl))
116 } else {
117 tracing::error!("invalid font size {font_size:?} => {font_size_px:?}");
118 child.layout(wl)
119 };
120 }
121 _ => {}
122 });
123 with_context_var(child, FONT_SIZE_VAR, size)
124}
125
126#[property(CONTEXT, default(FONT_WEIGHT_VAR), widget_impl(FontMix<P>))]
132pub fn font_weight(child: impl UiNode, weight: impl IntoVar<FontWeight>) -> impl UiNode {
133 with_context_var(child, FONT_WEIGHT_VAR, weight)
134}
135
136#[property(CONTEXT, default(FONT_STYLE_VAR), widget_impl(FontMix<P>))]
142pub fn font_style(child: impl UiNode, style: impl IntoVar<FontStyle>) -> impl UiNode {
143 with_context_var(child, FONT_STYLE_VAR, style)
144}
145
146#[property(CONTEXT, default(FONT_STRETCH_VAR), widget_impl(FontMix<P>))]
152pub fn font_stretch(child: impl UiNode, stretch: impl IntoVar<FontStretch>) -> impl UiNode {
153 with_context_var(child, FONT_STRETCH_VAR, stretch)
154}
155
156#[property(CONTEXT, default(FONT_SYNTHESIS_VAR), widget_impl(FontMix<P>))]
166pub fn font_synthesis(child: impl UiNode, enabled: impl IntoVar<FontSynthesis>) -> impl UiNode {
167 with_context_var(child, FONT_SYNTHESIS_VAR, enabled)
168}
169
170#[property(CONTEXT, default(FONT_AA_VAR), widget_impl(FontMix<P>))]
176pub fn font_aa(child: impl UiNode, aa: impl IntoVar<FontAntiAliasing>) -> impl UiNode {
177 with_context_var(child, FONT_AA_VAR, aa)
178}
179
180#[widget_mixin]
186pub struct TextFillMix<P>(P);
187
188context_var! {
189 pub static FONT_COLOR_VAR: Rgba = COLOR_SCHEME_VAR.map(|s| match s {
193 ColorScheme::Light => colors::BLACK,
194 ColorScheme::Dark => colors::WHITE,
195 });
196
197 pub static FONT_PALETTE_VAR: FontColorPalette = COLOR_SCHEME_VAR.map_into();
201
202 pub static FONT_PALETTE_COLORS_VAR: Vec<(u16, Rgba)> = vec![];
204}
205
206impl TextFillMix<()> {
207 pub fn context_vars_set(set: &mut ContextValueSet) {
209 set.insert(&FONT_COLOR_VAR);
210 set.insert(&FONT_PALETTE_VAR);
211 set.insert(&FONT_PALETTE_COLORS_VAR);
212 }
213}
214
215#[property(CONTEXT, default(FONT_COLOR_VAR), widget_impl(TextFillMix<P>))]
224pub fn font_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
225 with_context_var(child, FONT_COLOR_VAR, color)
226}
227
228#[property(CONTEXT, default(FONT_PALETTE_VAR), widget_impl(TextFillMix<P>))]
238pub fn font_palette(child: impl UiNode, palette: impl IntoVar<FontColorPalette>) -> impl UiNode {
239 with_context_var(child, FONT_PALETTE_VAR, palette)
240}
241
242pub fn with_font_palette_color(child: impl UiNode, index: u16, color: impl IntoVar<Rgba>) -> impl UiNode {
251 with_context_var(
252 child,
253 FONT_PALETTE_COLORS_VAR,
254 merge_var!(FONT_PALETTE_COLORS_VAR, color.into_var(), move |set, color| {
255 let mut set = set.clone();
256 if let Some(i) = set.iter().position(|(i, _)| *i == index) {
257 set[i].1 = *color;
258 } else {
259 set.push((index, *color));
260 }
261 set
262 }),
263 )
264}
265
266#[property(CONTEXT, default(FONT_PALETTE_COLORS_VAR), widget_impl(TextFillMix<P>))]
275pub fn font_palette_colors(child: impl UiNode, colors: impl IntoVar<Vec<(u16, Rgba)>>) -> impl UiNode {
276 with_context_var(child, FONT_PALETTE_COLORS_VAR, colors)
277}
278
279#[widget_mixin]
285pub struct TextAlignMix<P>(P);
286
287context_var! {
288 pub static TEXT_ALIGN_VAR: Align = Align::START;
290
291 pub static TEXT_OVERFLOW_ALIGN_VAR: Align = Align::TOP_START;
293
294 pub static JUSTIFY_MODE_VAR: Justify = Justify::Auto;
296}
297
298impl TextAlignMix<()> {
299 pub fn context_vars_set(set: &mut ContextValueSet) {
301 set.insert(&TEXT_ALIGN_VAR);
302 set.insert(&TEXT_OVERFLOW_ALIGN_VAR);
303 set.insert(&JUSTIFY_MODE_VAR);
304 }
305}
306
307#[property(CONTEXT, default(TEXT_ALIGN_VAR), widget_impl(TextAlignMix<P>))]
322pub fn txt_align(child: impl UiNode, mode: impl IntoVar<Align>) -> impl UiNode {
323 with_context_var(child, TEXT_ALIGN_VAR, mode)
324}
325
326#[property(CONTEXT, default(TEXT_OVERFLOW_ALIGN_VAR), widget_impl(TextAlignMix<P>))]
339pub fn txt_overflow_align(child: impl UiNode, mode: impl IntoVar<Align>) -> impl UiNode {
340 with_context_var(child, TEXT_OVERFLOW_ALIGN_VAR, mode)
341}
342
343#[property(CONTEXT, default(JUSTIFY_MODE_VAR), widget_impl(TextAlignMix<P>))]
352pub fn justify_mode(child: impl UiNode, mode: impl IntoVar<Justify>) -> impl UiNode {
353 with_context_var(child, JUSTIFY_MODE_VAR, mode)
354}
355
356#[widget_mixin]
362pub struct TextWrapMix<P>(P);
363
364context_var! {
365 pub static TEXT_WRAP_VAR: bool = true;
371
372 pub static WORD_BREAK_VAR: WordBreak = WordBreak::Normal;
374
375 pub static LINE_BREAK_VAR: LineBreak = LineBreak::Auto;
377
378 pub static HYPHENS_VAR: Hyphens = Hyphens::default();
380
381 pub static HYPHEN_CHAR_VAR: Txt = Txt::from_char('-');
383
384 pub static TEXT_OVERFLOW_VAR: TextOverflow = TextOverflow::Ignore;
386}
387
388impl TextWrapMix<()> {
389 pub fn context_vars_set(set: &mut ContextValueSet) {
391 set.insert(&TEXT_WRAP_VAR);
392 set.insert(&WORD_BREAK_VAR);
393 set.insert(&LINE_BREAK_VAR);
394 set.insert(&HYPHENS_VAR);
395 set.insert(&HYPHEN_CHAR_VAR);
396 set.insert(&TEXT_OVERFLOW_VAR);
397 }
398}
399
400#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
402pub enum TextOverflow {
403 Ignore,
411 Truncate(Txt),
416}
417impl fmt::Debug for TextOverflow {
418 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419 if f.alternate() {
420 write!(f, "TextOverflow")?
421 }
422 match self {
423 Self::Ignore => write!(f, "Ignore"),
424 Self::Truncate(arg0) => f.debug_tuple("Truncate").field(arg0).finish(),
425 }
426 }
427}
428impl TextOverflow {
429 pub fn truncate() -> Self {
431 Self::Truncate(Txt::from_static(""))
432 }
433
434 pub fn ellipses() -> Self {
436 Self::Truncate(Txt::from_char('…'))
437 }
438}
439impl_from_and_into_var! {
440 fn from(truncate: bool) -> TextOverflow {
442 if truncate {
443 TextOverflow::truncate()
444 } else {
445 TextOverflow::Ignore
446 }
447 }
448
449 fn from(truncate: Txt) -> TextOverflow {
450 TextOverflow::Truncate(truncate)
451 }
452 fn from(s: &'static str) -> TextOverflow {
453 Txt::from(s).into()
454 }
455 fn from(s: String) -> TextOverflow {
456 Txt::from(s).into()
457 }
458 fn from(c: char) -> TextOverflow {
459 Txt::from(c).into()
460 }
461}
462
463#[property(CONTEXT, default(TEXT_WRAP_VAR), widget_impl(TextWrapMix<P>))]
474pub fn txt_wrap(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
475 with_context_var(child, TEXT_WRAP_VAR, enabled)
476}
477
478#[property(CONTEXT, default(WORD_BREAK_VAR), widget_impl(TextWrapMix<P>))]
488pub fn word_break(child: impl UiNode, mode: impl IntoVar<WordBreak>) -> impl UiNode {
489 with_context_var(child, WORD_BREAK_VAR, mode)
490}
491
492#[property(CONTEXT, default(LINE_BREAK_VAR), widget_impl(TextWrapMix<P>))]
496pub fn line_break(child: impl UiNode, mode: impl IntoVar<LineBreak>) -> impl UiNode {
497 with_context_var(child, LINE_BREAK_VAR, mode)
498}
499
500#[property(CONTEXT, default(HYPHENS_VAR), widget_impl(TextWrapMix<P>))]
510pub fn hyphens(child: impl UiNode, hyphens: impl IntoVar<Hyphens>) -> impl UiNode {
511 with_context_var(child, HYPHENS_VAR, hyphens)
512}
513
514#[property(CONTEXT, default(HYPHEN_CHAR_VAR), widget_impl(TextWrapMix<P>))]
520pub fn hyphen_char(child: impl UiNode, hyphen: impl IntoVar<Txt>) -> impl UiNode {
521 with_context_var(child, HYPHEN_CHAR_VAR, hyphen)
522}
523
524#[property(CONTEXT, default(TEXT_OVERFLOW_VAR), widget_impl(TextWrapMix<P>))]
532pub fn txt_overflow(child: impl UiNode, overflow: impl IntoVar<TextOverflow>) -> impl UiNode {
533 with_context_var(child, TEXT_OVERFLOW_VAR, overflow)
534}
535
536#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
538pub fn is_overflown(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
539 let state = state.into_var();
540 match_node(child, move |_, op| match op {
541 UiNodeOp::Deinit => {
542 let _ = state.set(false);
543 }
544 UiNodeOp::Layout { .. } => {
545 let is_o = super::node::TEXT.laidout().overflow.is_some();
546 if is_o != state.get() {
547 let _ = state.set(is_o);
548 }
549 }
550 _ => {}
551 })
552}
553
554#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
559pub fn is_line_overflown(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
560 let state = state.into_var();
561 match_node(child, move |_, op| match op {
562 UiNodeOp::Deinit => {
563 let _ = state.set(false);
564 }
565 UiNodeOp::Layout { .. } => {
566 let txt = super::node::TEXT.laidout();
567 let is_o = if let Some(info) = &txt.overflow {
568 info.line < txt.shaped_text.lines_len().saturating_sub(1)
569 } else {
570 false
571 };
572 if is_o != state.get() {
573 let _ = state.set(is_o);
574 }
575 }
576 _ => {}
577 })
578}
579
580#[property(CHILD_LAYOUT+100, widget_impl(TextWrapMix<P>))]
586pub fn get_overflow(child: impl UiNode, txt: impl IntoVar<Txt>) -> impl UiNode {
587 let txt = txt.into_var();
588 match_node(child, move |_, op| {
589 if let UiNodeOp::Layout { .. } = op {
590 let l_txt = super::node::TEXT.laidout();
591 if let Some(info) = &l_txt.overflow {
592 let r = super::node::TEXT.resolved();
593 let tail = &r.segmented_text.text()[info.text_char..];
594 if txt.with(|t| t != tail) {
595 let _ = txt.set(Txt::from_str(tail));
596 }
597 } else if txt.with(|t| !t.is_empty()) {
598 let _ = txt.set(Txt::from_static(""));
599 }
600 }
601 })
602}
603
604#[widget_mixin]
610pub struct TextDecorationMix<P>(P);
611
612bitflags! {
613 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
615 #[serde(transparent)]
616 pub struct UnderlineSkip: u8 {
617 const NONE = 0;
619
620 const SPACES = 0b0001;
622
623 const GLYPHS = 0b0010;
625
626 const DEFAULT = Self::GLYPHS.bits();
628 }
629}
630impl Default for UnderlineSkip {
631 fn default() -> Self {
632 Self::DEFAULT
633 }
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
638pub enum UnderlinePosition {
639 #[default]
641 Font,
642 Descent,
644}
645
646context_var! {
647 pub static UNDERLINE_THICKNESS_VAR: UnderlineThickness = 0;
649 pub static UNDERLINE_STYLE_VAR: LineStyle = LineStyle::Hidden;
651 pub static UNDERLINE_COLOR_VAR: Rgba = FONT_COLOR_VAR;
653 pub static UNDERLINE_SKIP_VAR: UnderlineSkip = UnderlineSkip::DEFAULT;
655 pub static UNDERLINE_POSITION_VAR: UnderlinePosition = UnderlinePosition::Font;
657
658 pub static OVERLINE_THICKNESS_VAR: TextLineThickness = 0;
660 pub static OVERLINE_STYLE_VAR: LineStyle = LineStyle::Hidden;
662 pub static OVERLINE_COLOR_VAR: Rgba = FONT_COLOR_VAR;
664
665 pub static STRIKETHROUGH_THICKNESS_VAR: TextLineThickness = 0;
667 pub static STRIKETHROUGH_STYLE_VAR: LineStyle = LineStyle::Hidden;
669 pub static STRIKETHROUGH_COLOR_VAR: Rgba = FONT_COLOR_VAR;
671
672 pub static IME_UNDERLINE_THICKNESS_VAR: UnderlineThickness = 1;
674 pub static IME_UNDERLINE_STYLE_VAR: LineStyle = LineStyle::Dotted;
676}
677
678impl TextDecorationMix<()> {
679 pub fn context_vars_set(set: &mut ContextValueSet) {
681 set.insert(&UNDERLINE_THICKNESS_VAR);
682 set.insert(&UNDERLINE_STYLE_VAR);
683 set.insert(&UNDERLINE_COLOR_VAR);
684 set.insert(&UNDERLINE_SKIP_VAR);
685 set.insert(&UNDERLINE_POSITION_VAR);
686 set.insert(&OVERLINE_THICKNESS_VAR);
687 set.insert(&OVERLINE_STYLE_VAR);
688 set.insert(&OVERLINE_COLOR_VAR);
689 set.insert(&STRIKETHROUGH_THICKNESS_VAR);
690 set.insert(&STRIKETHROUGH_STYLE_VAR);
691 set.insert(&STRIKETHROUGH_COLOR_VAR);
692 set.insert(&IME_UNDERLINE_THICKNESS_VAR);
693 set.insert(&IME_UNDERLINE_STYLE_VAR);
694 }
695}
696
697#[property(CONTEXT, default(UNDERLINE_THICKNESS_VAR, UNDERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
701pub fn underline(child: impl UiNode, thickness: impl IntoVar<UnderlineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
702 let child = with_context_var(child, UNDERLINE_THICKNESS_VAR, thickness);
703 with_context_var(child, UNDERLINE_STYLE_VAR, style)
704}
705#[property(CONTEXT, default(UNDERLINE_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
710pub fn underline_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
711 with_context_var(child, UNDERLINE_COLOR_VAR, color)
712}
713#[property(CONTEXT, default(UNDERLINE_SKIP_VAR), widget_impl(TextDecorationMix<P>))]
719pub fn underline_skip(child: impl UiNode, skip: impl IntoVar<UnderlineSkip>) -> impl UiNode {
720 with_context_var(child, UNDERLINE_SKIP_VAR, skip)
721}
722#[property(CONTEXT, default(UNDERLINE_POSITION_VAR), widget_impl(TextDecorationMix<P>))]
729pub fn underline_position(child: impl UiNode, position: impl IntoVar<UnderlinePosition>) -> impl UiNode {
730 with_context_var(child, UNDERLINE_POSITION_VAR, position)
731}
732
733#[property(CONTEXT, default(OVERLINE_THICKNESS_VAR, OVERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
737pub fn overline(child: impl UiNode, thickness: impl IntoVar<TextLineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
738 let child = with_context_var(child, OVERLINE_THICKNESS_VAR, thickness);
739 with_context_var(child, OVERLINE_STYLE_VAR, style)
740}
741#[property(CONTEXT, default(OVERLINE_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
746pub fn overline_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
747 with_context_var(child, OVERLINE_COLOR_VAR, color)
748}
749
750#[property(CONTEXT, default(STRIKETHROUGH_THICKNESS_VAR, STRIKETHROUGH_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
754pub fn strikethrough(child: impl UiNode, thickness: impl IntoVar<TextLineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
755 let child = with_context_var(child, STRIKETHROUGH_THICKNESS_VAR, thickness);
756 with_context_var(child, STRIKETHROUGH_STYLE_VAR, style)
757}
758#[property(CONTEXT, default(STRIKETHROUGH_COLOR_VAR), widget_impl(TextDecorationMix<P>))]
763pub fn strikethrough_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
764 with_context_var(child, STRIKETHROUGH_COLOR_VAR, color)
765}
766
767#[property(CONTEXT, default(IME_UNDERLINE_THICKNESS_VAR, IME_UNDERLINE_STYLE_VAR), widget_impl(TextDecorationMix<P>))]
771pub fn ime_underline(child: impl UiNode, thickness: impl IntoVar<UnderlineThickness>, style: impl IntoVar<LineStyle>) -> impl UiNode {
772 let child = with_context_var(child, IME_UNDERLINE_THICKNESS_VAR, thickness);
773 with_context_var(child, IME_UNDERLINE_STYLE_VAR, style)
774}
775
776#[widget_mixin]
784pub struct TextSpacingMix<P>(P);
785
786context_var! {
787 pub static LINE_HEIGHT_VAR: LineHeight = LineHeight::Default;
791
792 pub static LINE_SPACING_VAR: LineSpacing = LineSpacing::Default;
796
797 pub static LETTER_SPACING_VAR: LetterSpacing = LetterSpacing::Default;
801
802 pub static WORD_SPACING_VAR: WordSpacing = WordSpacing::Default;
806
807 pub static TAB_LENGTH_VAR: TabLength = 400.pct();
809}
810
811impl TextSpacingMix<()> {
812 pub fn context_vars_set(set: &mut ContextValueSet) {
814 set.insert(&LINE_HEIGHT_VAR);
815 set.insert(&LINE_SPACING_VAR);
816 set.insert(&LETTER_SPACING_VAR);
817 set.insert(&WORD_SPACING_VAR);
818 set.insert(&TAB_LENGTH_VAR);
819 }
820}
821
822#[property(CONTEXT, default(LINE_HEIGHT_VAR), widget_impl(TextSpacingMix<P>))]
834pub fn line_height(child: impl UiNode, height: impl IntoVar<LineHeight>) -> impl 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 UiNode, extra: impl IntoVar<LetterSpacing>) -> impl 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 UiNode, extra: impl IntoVar<LineSpacing>) -> impl 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 UiNode, extra: impl IntoVar<WordSpacing>) -> impl 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 UiNode, length: impl IntoVar<TabLength>) -> impl 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 UiNode, transform: impl IntoVar<WhiteSpace>) -> impl 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 UiNode, transform: impl IntoVar<TextTransformFn>) -> impl 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 UiNode, lang: impl IntoVar<Langs>) -> impl 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 UiNode, direction: impl IntoVar<LayoutDirection>) -> impl 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 UiNode, name: FontVariationName, value: impl IntoVar<f32>) -> impl 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) -> impl UiNode
1072where
1073 C: UiNode,
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 UiNode, variations: impl IntoVar<FontVariations>) -> impl 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 UiNode, features: impl IntoVar<FontFeatures>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<CapsVariant>) -> impl 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 UiNode, state: impl IntoVar<NumVariant>) -> impl 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 UiNode, state: impl IntoVar<NumSpacing>) -> impl 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 UiNode, state: impl IntoVar<NumFraction>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontFeatureState>) -> impl 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 UiNode, state: impl IntoVar<FontStyleSet>) -> impl 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 UiNode, state: impl IntoVar<CharVariant>) -> impl 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 UiNode, state: impl IntoVar<FontPosition>) -> impl 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 UiNode, state: impl IntoVar<JpVariant>) -> impl 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 UiNode, state: impl IntoVar<CnVariant>) -> impl 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 UiNode, state: impl IntoVar<EastAsianWidth>) -> impl 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
1243 pub static ACCEPTS_TAB_VAR: bool = false;
1245
1246 pub static ACCEPTS_ENTER_VAR: bool = false;
1248
1249 pub static CARET_COLOR_VAR: Rgba = FONT_COLOR_VAR;
1251
1252 pub static INTERACTIVE_CARET_VISUAL_VAR: WidgetFn<CaretShape> = wgt_fn!(|s| super::node::default_interactive_caret_visual(s));
1254
1255 pub static INTERACTIVE_CARET_VAR: InteractiveCaretMode = InteractiveCaretMode::default();
1257
1258 pub static SELECTION_COLOR_VAR: Rgba = colors::AZURE.with_alpha(30.pct());
1260
1261 pub static TXT_PARSE_LIVE_VAR: bool = true;
1263
1264 pub static CHANGE_STOP_DELAY_VAR: Duration = 1.secs();
1266
1267 pub static AUTO_SELECTION_VAR: AutoSelection = AutoSelection::default();
1269
1270 pub static MAX_CHARS_COUNT_VAR: usize = 0;
1274
1275 pub static OBSCURING_CHAR_VAR: char = '•';
1277
1278 pub static OBSCURE_TXT_VAR: bool = false;
1280
1281 pub(super) static TXT_PARSE_PENDING_VAR: bool = false;
1282}
1283
1284impl TextEditMix<()> {
1285 pub fn context_vars_set(set: &mut ContextValueSet) {
1287 set.insert(&TEXT_EDITABLE_VAR);
1288 set.insert(&TEXT_SELECTABLE_VAR);
1289 set.insert(&ACCEPTS_ENTER_VAR);
1290 set.insert(&CARET_COLOR_VAR);
1291 set.insert(&INTERACTIVE_CARET_VISUAL_VAR);
1292 set.insert(&INTERACTIVE_CARET_VAR);
1293 set.insert(&SELECTION_COLOR_VAR);
1294 set.insert(&TXT_PARSE_LIVE_VAR);
1295 set.insert(&CHANGE_STOP_DELAY_VAR);
1296 set.insert(&AUTO_SELECTION_VAR);
1297 set.insert(&MAX_CHARS_COUNT_VAR);
1298 set.insert(&OBSCURING_CHAR_VAR);
1299 set.insert(&OBSCURE_TXT_VAR);
1300 }
1301}
1302
1303#[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1307pub enum CaretShape {
1308 SelectionLeft,
1310 SelectionRight,
1312 Insert,
1314}
1315impl fmt::Debug for CaretShape {
1316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1317 if f.alternate() {
1318 write!(f, "CaretShape::")?;
1319 }
1320 match self {
1321 Self::SelectionLeft => write!(f, "SelectionLeft"),
1322 Self::SelectionRight => write!(f, "SelectionRight"),
1323 Self::Insert => write!(f, "Insert"),
1324 }
1325 }
1326}
1327
1328#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1330pub enum InteractiveCaretMode {
1331 #[default]
1333 TouchOnly,
1334 Enabled,
1336 Disabled,
1338}
1339impl fmt::Debug for InteractiveCaretMode {
1340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1341 if f.alternate() {
1342 write!(f, "InteractiveCaretMode::")?;
1343 }
1344 match self {
1345 Self::TouchOnly => write!(f, "TouchOnly"),
1346 Self::Enabled => write!(f, "Enabled"),
1347 Self::Disabled => write!(f, "Disabled"),
1348 }
1349 }
1350}
1351impl_from_and_into_var! {
1352 fn from(enabled: bool) -> InteractiveCaretMode {
1353 if enabled {
1354 InteractiveCaretMode::Enabled
1355 } else {
1356 InteractiveCaretMode::Disabled
1357 }
1358 }
1359}
1360
1361#[property(CONTEXT, default(TEXT_EDITABLE_VAR), widget_impl(TextEditMix<P>))]
1368pub fn txt_editable(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1369 with_context_var(child, TEXT_EDITABLE_VAR, enabled)
1370}
1371
1372#[property(CONTEXT, default(TEXT_SELECTABLE_VAR), widget_impl(TextEditMix<P>))]
1376pub fn txt_selectable(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1377 with_context_var(child, TEXT_SELECTABLE_VAR, enabled)
1378}
1379
1380#[property(CONTEXT, default(ACCEPTS_TAB_VAR), widget_impl(TextEditMix<P>))]
1386pub fn accepts_tab(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1387 with_context_var(child, ACCEPTS_TAB_VAR, enabled)
1388}
1389
1390#[property(CONTEXT, default(ACCEPTS_ENTER_VAR), widget_impl(TextEditMix<P>))]
1394pub fn accepts_enter(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1395 with_context_var(child, ACCEPTS_ENTER_VAR, enabled)
1396}
1397
1398#[property(CONTEXT, default(CARET_COLOR_VAR), widget_impl(TextEditMix<P>))]
1402pub fn caret_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
1403 with_context_var(child, CARET_COLOR_VAR, color)
1404}
1405
1406#[property(CONTEXT, default(INTERACTIVE_CARET_VISUAL_VAR), widget_impl(TextEditMix<P>))]
1419pub fn interactive_caret_visual(child: impl UiNode, visual: impl IntoVar<WidgetFn<CaretShape>>) -> impl UiNode {
1420 with_context_var(child, INTERACTIVE_CARET_VISUAL_VAR, visual)
1421}
1422
1423#[property(CONTEXT, default(INTERACTIVE_CARET_VAR), widget_impl(TextEditMix<P>))]
1427pub fn interactive_caret(child: impl UiNode, mode: impl IntoVar<InteractiveCaretMode>) -> impl UiNode {
1428 with_context_var(child, INTERACTIVE_CARET_VAR, mode)
1429}
1430
1431#[property(CONTEXT, default(SELECTION_COLOR_VAR), widget_impl(TextEditMix<P>))]
1433pub fn selection_color(child: impl UiNode, color: impl IntoVar<Rgba>) -> impl UiNode {
1434 with_context_var(child, SELECTION_COLOR_VAR, color)
1435}
1436
1437#[property(CONTEXT, default(TXT_PARSE_LIVE_VAR), widget_impl(TextEditMix<P>))]
1446pub fn txt_parse_live(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1447 with_context_var(child, TXT_PARSE_LIVE_VAR, enabled)
1448}
1449
1450#[property(EVENT, widget_impl(TextEditMix<P>))]
1457pub fn txt_parse_on_stop(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1458 let enabled = enabled.into_var();
1459 let child = txt_parse_live(child, enabled.map(|&b| !b));
1460 on_change_stop(
1461 child,
1462 hn!(|_| {
1463 if enabled.get() {
1464 super::cmd::PARSE_CMD.scoped(WIDGET.id()).notify();
1465 }
1466 }),
1467 )
1468}
1469
1470#[property(CONTEXT, default(MAX_CHARS_COUNT_VAR), widget_impl(TextEditMix<P>))]
1476pub fn max_chars_count(child: impl UiNode, max: impl IntoVar<usize>) -> impl UiNode {
1477 with_context_var(child, MAX_CHARS_COUNT_VAR, max)
1478}
1479
1480#[property(CONTEXT, default(false), widget_impl(TextEditMix<P>))]
1487pub fn is_parse_pending(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
1488 with_context_var(child, TXT_PARSE_PENDING_VAR, state)
1490}
1491
1492#[property(EVENT, widget_impl(TextEditMix<P>))]
1501pub fn on_change_stop(child: impl UiNode, handler: impl WidgetHandler<ChangeStopArgs>) -> impl UiNode {
1502 super::node::on_change_stop(child, handler)
1503}
1504
1505#[property(CONTEXT, default(CHANGE_STOP_DELAY_VAR), widget_impl(TextEditMix<P>))]
1516pub fn change_stop_delay(child: impl UiNode, delay: impl IntoVar<Duration>) -> impl UiNode {
1517 with_context_var(child, CHANGE_STOP_DELAY_VAR, delay)
1518}
1519
1520#[property(CONTEXT, default(AUTO_SELECTION_VAR), widget_impl(TextEditMix<P>))]
1524pub fn auto_selection(child: impl UiNode, mode: impl IntoVar<AutoSelection>) -> impl UiNode {
1525 with_context_var(child, AUTO_SELECTION_VAR, mode)
1526}
1527
1528#[property(CONTEXT, default(OBSCURING_CHAR_VAR), widget_impl(TextEditMix<P>))]
1534pub fn obscuring_char(child: impl UiNode, character: impl IntoVar<char>) -> impl UiNode {
1535 with_context_var(child, OBSCURING_CHAR_VAR, character)
1536}
1537
1538#[property(CONTEXT, default(OBSCURE_TXT_VAR), widget_impl(TextEditMix<P>))]
1549pub fn obscure_txt(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
1550 with_context_var(child, OBSCURE_TXT_VAR, enabled)
1551}
1552
1553bitflags! {
1554 #[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
1556 pub struct AutoSelection: u8 {
1557 const DISABLED = 0;
1559 const CLEAR_ON_BLUR = 0b0000_0001;
1563 const ALL_ON_FOCUS_KEYBOARD = 0b0000_00010;
1565 const ALL_ON_FOCUS_POINTER = 0b0000_00100;
1568 const ENABLED = 0b1111_1111;
1570 }
1571}
1572impl_from_and_into_var! {
1573 fn from(enabled: bool) -> AutoSelection {
1574 if enabled {
1575 AutoSelection::ENABLED
1576 } else {
1577 AutoSelection::DISABLED
1578 }
1579 }
1580}
1581impl Default for AutoSelection {
1582 fn default() -> Self {
1583 Self::CLEAR_ON_BLUR
1584 }
1585}
1586
1587#[derive(Debug, Clone)]
1591pub struct ChangeStopArgs {
1592 pub cause: ChangeStopCause,
1594}
1595
1596#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1600pub enum ChangeStopCause {
1601 DelayElapsed,
1605 Enter,
1610 Blur,
1612}
1613
1614#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)]
1616pub struct CaretStatus {
1617 index: usize,
1618 line: u32,
1619 column: u32,
1620}
1621impl CaretStatus {
1622 pub fn none() -> Self {
1624 Self {
1625 index: usize::MAX,
1626 line: 0,
1627 column: 0,
1628 }
1629 }
1630
1631 pub fn new(index: usize, text: &SegmentedText) -> Self {
1637 assert!(index <= text.text().len());
1638
1639 if text.text().is_empty() {
1640 Self { line: 1, column: 1, index }
1641 } else {
1642 let mut line = 1;
1643 let mut line_start = 0;
1644 for seg in text.segs() {
1645 if seg.end > index {
1646 break;
1647 }
1648 if let TextSegmentKind::LineBreak = seg.kind {
1649 line += 1;
1650 line_start = seg.end;
1651 }
1652 }
1653
1654 let column = text.text()[line_start..index].chars().count() + 1;
1655
1656 Self {
1657 line,
1658 column: column as _,
1659 index,
1660 }
1661 }
1662 }
1663
1664 pub fn index(&self) -> Option<usize> {
1666 match self.index {
1667 usize::MAX => None,
1668 i => Some(i),
1669 }
1670 }
1671
1672 pub fn line(&self) -> Option<NonZeroU32> {
1676 NonZeroU32::new(self.line)
1677 }
1678
1679 pub fn column(&self) -> Option<NonZeroU32> {
1683 NonZeroU32::new(self.column)
1684 }
1685}
1686impl fmt::Display for CaretStatus {
1687 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1688 if self.index().is_some() {
1689 write!(f, "Ln {}, Col {}", self.line, self.column)
1690 } else {
1691 Ok(())
1692 }
1693 }
1694}
1695
1696#[derive(Debug, Clone, PartialEq, Eq)]
1698pub enum LinesWrapCount {
1699 NoWrap(usize),
1703 Wrap(Vec<u32>),
1707}
1708impl LinesWrapCount {
1709 pub fn lines_len(&self) -> usize {
1711 match self {
1712 Self::NoWrap(l) => *l,
1713 Self::Wrap(lns) => lns.len(),
1714 }
1715 }
1716}
1717
1718#[widget_mixin]
1725pub struct ParagraphMix<P>(P);
1726
1727context_var! {
1728 pub static PARAGRAPH_SPACING_VAR: ParagraphSpacing = 1.em();
1730}
1731
1732impl ParagraphMix<()> {
1733 pub fn context_vars_set(set: &mut ContextValueSet) {
1735 set.insert(&PARAGRAPH_SPACING_VAR);
1736 }
1737}
1738
1739#[property(CONTEXT, default(PARAGRAPH_SPACING_VAR), widget_impl(ParagraphMix<P>))]
1748pub fn paragraph_spacing(child: impl UiNode, extra: impl IntoVar<ParagraphSpacing>) -> impl UiNode {
1749 with_context_var(child, PARAGRAPH_SPACING_VAR, extra)
1750}
1751
1752#[widget_mixin]
1754pub struct SelectionToolbarMix<P>(P);
1755
1756context_var! {
1757 pub static SELECTION_TOOLBAR_FN_VAR: WidgetFn<SelectionToolbarArgs> = WidgetFn::nil();
1759 pub static SELECTION_TOOLBAR_ANCHOR_VAR: AnchorOffset = AnchorOffset::out_top();
1761}
1762
1763impl SelectionToolbarMix<()> {
1764 pub fn context_vars_set(set: &mut ContextValueSet) {
1766 set.insert(&SELECTION_TOOLBAR_FN_VAR);
1767 set.insert(&SELECTION_TOOLBAR_ANCHOR_VAR);
1768 }
1769}
1770
1771#[property(CONTEXT, widget_impl(SelectionToolbarMix<P>))]
1775pub fn selection_toolbar(child: impl UiNode, toolbar: impl UiNode) -> impl UiNode {
1776 selection_toolbar_fn(child, WidgetFn::singleton(toolbar))
1777}
1778
1779#[property(CONTEXT, default(SELECTION_TOOLBAR_FN_VAR), widget_impl(SelectionToolbarMix<P>))]
1783pub fn selection_toolbar_fn(child: impl UiNode, toolbar: impl IntoVar<WidgetFn<SelectionToolbarArgs>>) -> impl UiNode {
1784 with_context_var(child, SELECTION_TOOLBAR_FN_VAR, toolbar)
1785}
1786
1787pub struct SelectionToolbarArgs {
1791 pub anchor_id: WidgetId,
1793 pub is_touch: bool,
1795}
1796
1797#[property(CONTEXT, default(SELECTION_TOOLBAR_ANCHOR_VAR), widget_impl(SelectionToolbarMix<P>))]
1803pub fn selection_toolbar_anchor(child: impl UiNode, offset: impl IntoVar<AnchorOffset>) -> impl UiNode {
1804 with_context_var(child, SELECTION_TOOLBAR_ANCHOR_VAR, offset)
1805}
1806
1807#[widget_mixin]
1809pub struct TextInspectMix<P>(P);
1810
1811impl TextInspectMix<()> {
1812 pub fn context_vars_set(set: &mut ContextValueSet) {
1814 let _ = set;
1815 }
1816}
1817
1818#[property(EVENT, default(None), widget_impl(TextInspectMix<P>))]
1820pub fn get_caret_index(child: impl UiNode, index: impl IntoVar<Option<CaretIndex>>) -> impl UiNode {
1821 super::node::get_caret_index(child, index)
1822}
1823
1824#[property(EVENT, default(CaretStatus::none()), widget_impl(TextInspectMix<P>))]
1826pub fn get_caret_status(child: impl UiNode, status: impl IntoVar<CaretStatus>) -> impl UiNode {
1827 super::node::get_caret_status(child, status)
1828}
1829
1830#[property(CHILD_LAYOUT+100, default(0), widget_impl(TextInspectMix<P>))]
1837pub fn get_lines_len(child: impl UiNode, len: impl IntoVar<usize>) -> impl UiNode {
1838 super::node::get_lines_len(child, len)
1839}
1840
1841#[property(CHILD_LAYOUT+100, default(LinesWrapCount::NoWrap(0)), widget_impl(TextInspectMix<P>))]
1843pub fn get_lines_wrap_count(child: impl UiNode, lines: impl IntoVar<LinesWrapCount>) -> impl UiNode {
1844 super::node::get_lines_wrap_count(child, lines)
1845}
1846
1847#[property(EVENT, default(0), widget_impl(TextInspectMix<P>))]
1849pub fn get_chars_count(child: impl UiNode, chars: impl IntoVar<usize>) -> impl UiNode {
1850 let chars = chars.into_var();
1851 match_node(child, move |_, op| {
1852 if let UiNodeOp::Init = op {
1853 let ctx = super::node::TEXT.resolved();
1854 let _ = chars.set_from_map(&ctx.txt, |t| t.chars().count());
1855 let handle = ctx.txt.bind_map(&chars, |t| t.chars().count());
1856 WIDGET.push_var_handle(handle);
1857 }
1858 })
1859}
1860
1861#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
1865pub fn txt_highlight(child: impl UiNode, range: impl IntoVar<std::ops::Range<CaretIndex>>, color: impl IntoVar<Rgba>) -> impl UiNode {
1866 let range = range.into_var();
1867 let color = color.into_var();
1868 let color_key = FrameValueKey::new_unique();
1869 match_node(child, move |_, op| match op {
1870 UiNodeOp::Init => {
1871 WIDGET.sub_var_render(&range).sub_var_render_update(&color);
1872 }
1873 UiNodeOp::Render { frame } => {
1874 let l_txt = super::node::TEXT.laidout();
1875 let r_txt = super::node::TEXT.resolved();
1876 let r_txt = r_txt.segmented_text.text();
1877
1878 for line_rect in l_txt.shaped_text.highlight_rects(range.get(), r_txt) {
1879 frame.push_color(line_rect, color_key.bind_var(&color, |c| *c));
1880 }
1881 }
1882 UiNodeOp::RenderUpdate { update } => {
1883 if let Some(color_update) = color_key.update_var(&color, |c| *c) {
1884 update.update_color(color_update)
1885 }
1886 }
1887 _ => {}
1888 })
1889}
1890
1891#[property(CHILD_LAYOUT+100, widget_impl(TextInspectMix<P>))]
1895pub fn get_font_use(child: impl UiNode, font_use: impl IntoVar<Vec<(Font, std::ops::Range<usize>)>>) -> impl UiNode {
1896 let font_use = font_use.into_var();
1897 let mut shaped_text_version = u32::MAX;
1898 match_node(child, move |c, op| {
1899 if let UiNodeOp::Layout { wl, final_size } = op {
1900 *final_size = c.layout(wl);
1901
1902 let ctx = crate::node::TEXT.laidout();
1903
1904 if ctx.shaped_text_version != shaped_text_version && font_use.capabilities().can_modify() {
1905 shaped_text_version = ctx.shaped_text_version;
1906
1907 let mut r = vec![];
1908
1909 for seg in ctx.shaped_text.lines().flat_map(|l| l.segs()) {
1910 let mut seg_glyph_i = 0;
1911 let seg_range = seg.text_range();
1912
1913 for (font, glyphs) in seg.glyphs() {
1914 if r.is_empty() {
1915 r.push((font.clone(), 0..seg_range.end));
1916 } else {
1917 let last_i = r.len() - 1;
1918 if &r[last_i].0 != font {
1919 let seg_char_i = seg.clusters()[seg_glyph_i] as usize;
1920
1921 let char_i = seg_range.start + seg_char_i;
1922 r[last_i].1.end = char_i;
1923 r.push((font.clone(), char_i..seg_range.end));
1924 }
1925 }
1926 seg_glyph_i += glyphs.len();
1927 }
1928 }
1929
1930 let _ = font_use.set(r);
1931 }
1932 }
1933 })
1934}