Skip to main content

zng_wgt_ansi_text/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! ANSI text widget, properties and nodes.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use zng_ext_font::*;
15use zng_wgt::{prelude::*, *};
16use zng_wgt_fill::*;
17use zng_wgt_filter::*;
18use zng_wgt_input::{CursorIcon, cursor};
19use zng_wgt_layer::popup;
20use zng_wgt_scroll::{LazyMode, lazy};
21use zng_wgt_stack::{Stack, StackDirection};
22use zng_wgt_text::*;
23
24#[doc(hidden)]
25pub use zng_wgt_text::__formatx;
26
27/// Render text styled using ANSI escape sequences.
28///
29/// Supports color, weight, italic and more, see [`AnsiStyle`] for the full style supported.
30#[widget($crate::AnsiText {
31    ($txt:literal) => {
32        txt = $crate::__formatx!($txt);
33    };
34    ($txt:expr) => {
35        txt = $txt;
36    };
37    ($txt:tt, $($format:tt)*) => {
38        txt = $crate::__formatx!($txt, $($format)*);
39    };
40})]
41#[rustfmt::skip]
42pub struct AnsiText(
43    FontMix<
44    TextSpacingMix<
45    ParagraphMix<
46    LangMix<
47    WidgetBase
48    >>>>
49);
50impl AnsiText {
51    fn widget_intrinsic(&mut self) {
52        widget_set! {
53            self;
54            font_family = ["JetBrains Mono", "Consolas", "monospace"];
55            rich_text = true;
56
57            popup::context_capture = default_popup_context_capture();
58            when #txt_selectable {
59                cursor = CursorIcon::Text;
60                zng_wgt_menu::context::context_menu_fn = WidgetFn::new(default_context_menu);
61            }
62        };
63
64        self.widget_builder().push_build_action(|wgt| {
65            let txt = wgt.capture_var_or_default(property_id!(txt));
66            let child = ansi_node(txt);
67            wgt.set_child(child);
68        });
69    }
70
71    widget_impl! {
72        /// ANSI text.
73        pub txt(text: impl IntoVar<Txt>);
74
75        /// Enable text selection, copy.
76        ///
77        /// Note that the copy is only in plain text, without the ANSI escape codes.
78        pub zng_wgt_text::txt_selectable(enabled: impl IntoVar<bool>);
79    }
80}
81
82pub use ansi_parse::*;
83mod ansi_parse {
84
85    use super::*;
86
87    /// Represents a segment of ANSI styled text that shares the same style.
88    #[derive(Debug)]
89    #[non_exhaustive]
90    pub struct AnsiTxt<'a> {
91        /// Text run.
92        pub txt: &'a str,
93        /// Text style.
94        pub style: AnsiStyle,
95    }
96
97    /// Represents the ANSI style of a text run.
98    ///
99    /// See [`AnsiText`](struct@super::AnsiText) for more details.
100    #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
101    #[non_exhaustive]
102    pub struct AnsiStyle {
103        /// Background color.
104        pub background_color: AnsiColor,
105        /// Font color.
106        pub color: AnsiColor,
107        /// Font weight.
108        pub weight: AnsiWeight,
109        /// Font italic.
110        pub italic: bool,
111        /// Underline.
112        pub underline: bool,
113        /// Strikethrough.
114        pub strikethrough: bool,
115        /// Negative color.
116        pub invert_color: bool,
117        /// Visibility.
118        pub hidden: bool,
119        /// Blink animation.
120        pub blink: bool,
121    }
122    impl Default for AnsiStyle {
123        fn default() -> Self {
124            Self {
125                background_color: AnsiColor::Black,
126                color: AnsiColor::White,
127                weight: Default::default(),
128                italic: false,
129                underline: false,
130                strikethrough: false,
131                invert_color: false,
132                hidden: false,
133                blink: false,
134            }
135        }
136    }
137
138    /// Named ANSI color.
139    ///
140    /// See [`AnsiStyle`] for more details.
141    #[allow(missing_docs)]
142    #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
143    pub enum AnsiColor {
144        Black,
145        Red,
146        Green,
147        Yellow,
148        Blue,
149        Magenta,
150        Cyan,
151        White,
152        /// Gray
153        BrightBlack,
154        BrightRed,
155        BrightGreen,
156        BrightYellow,
157        BrightBlue,
158        BrightMagenta,
159        BrightCyan,
160        BrightWhite,
161        /// 8-bit lookup.
162        Ansi256(u8),
163        /// RGB
164        TrueColor(u8, u8, u8),
165    }
166    impl_from_and_into_var! {
167        fn from(color: AnsiColor) -> Rgba {
168            match color {
169                AnsiColor::Black => rgb(0, 0, 0),
170                AnsiColor::Red => rgb(205, 49, 49),
171                AnsiColor::Green => rgb(13, 188, 121),
172                AnsiColor::Yellow => rgb(229, 229, 16),
173                AnsiColor::Blue => rgb(36, 114, 200),
174                AnsiColor::Magenta => rgb(188, 63, 188),
175                AnsiColor::Cyan => rgb(17, 168, 205),
176                AnsiColor::White => rgb(229, 229, 229),
177                AnsiColor::BrightBlack => rgb(102, 102, 102),
178                AnsiColor::BrightRed => rgb(241, 76, 76),
179                AnsiColor::BrightGreen => rgb(35, 209, 139),
180                AnsiColor::BrightYellow => rgb(245, 245, 67),
181                AnsiColor::BrightBlue => rgb(59, 142, 234),
182                AnsiColor::BrightMagenta => rgb(214, 112, 214),
183                AnsiColor::BrightCyan => rgb(41, 184, 219),
184                AnsiColor::BrightWhite => rgb(229, 229, 229),
185                AnsiColor::Ansi256(c) => {
186                    let (r, g, b) = X_TERM_256[c as usize];
187                    rgb(r, g, b)
188                }
189                AnsiColor::TrueColor(r, g, b) => rgb(r, g, b),
190            }
191        }
192    }
193
194    /// Font weight defined by ANSI escape codes.
195    ///
196    /// See [`AnsiStyle`] for more details.
197    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
198    pub enum AnsiWeight {
199        /// Normal.
200        #[default]
201        Normal,
202        /// Bold.
203        Bold,
204        /// Light.
205        Faint,
206    }
207    impl_from_and_into_var! {
208        fn from(weight: AnsiWeight) -> FontWeight {
209            match weight {
210                AnsiWeight::Normal => FontWeight::NORMAL,
211                AnsiWeight::Bold => FontWeight::BOLD,
212                AnsiWeight::Faint => FontWeight::LIGHT,
213            }
214        }
215    }
216
217    /// Iterator that parses ANSI escape codes.
218    ///
219    /// This is the pull style parser used internally by the [`AnsiText!`] widget.
220    ///
221    /// [`AnsiText!`]: struct@crate::AnsiText
222    pub struct AnsiTextParser<'a> {
223        source: &'a str,
224        /// Current style.
225        pub style: AnsiStyle,
226
227        esc: &'a [&'a str],
228    }
229    impl<'a> AnsiTextParser<'a> {
230        /// New parsing iterator.
231        pub fn new(source: &'a str) -> Self {
232            Self::with_esc(source, &["\x1b"])
233        }
234
235        /// New parsing iterator with custom ESC markers.
236        pub fn with_esc(source: &'a str, esc: &'a [&'a str]) -> Self {
237            Self {
238                source,
239                style: AnsiStyle::default(),
240                esc,
241            }
242        }
243    }
244    impl<'a> Iterator for AnsiTextParser<'a> {
245        type Item = AnsiTxt<'a>;
246
247        fn next(&mut self) -> Option<Self::Item> {
248            fn is_esc_end(byte: u8) -> bool {
249                (0x40..=0x7e).contains(&byte)
250            }
251            fn strip_esc<'a>(s: &'a str, esc: &[&str]) -> Option<&'a str> {
252                for esc in esc {
253                    if esc.is_empty() {
254                        continue;
255                    }
256                    if let Some(s) = s.strip_prefix(esc)
257                        && let Some(s) = s.strip_prefix('[')
258                    {
259                        return Some(s);
260                    }
261                }
262                None
263            }
264            fn find_esc(s: &str, esc: &[&str]) -> Option<usize> {
265                for esc in esc {
266                    if esc.is_empty() {
267                        continue;
268                    }
269                    let mut s = s;
270                    if let Some(i) = s.find(esc) {
271                        s = &s[i + esc.len()..];
272                        if s.starts_with('[') {
273                            return Some(i);
274                        }
275                    } else {
276                        continue;
277                    }
278                }
279                None
280            }
281
282            loop {
283                if self.source.is_empty() {
284                    return None;
285                } else if let Some(source) = strip_esc(self.source, self.esc) {
286                    let mut esc_end = 0;
287                    while esc_end < source.len() && !is_esc_end(source.as_bytes()[esc_end]) {
288                        esc_end += 1;
289                    }
290                    esc_end += 1;
291
292                    let (esc, source) = source.split_at(esc_end);
293
294                    let esc = &esc[..(esc.len() - 1)];
295                    self.style.set(esc);
296
297                    self.source = source;
298                    continue;
299                } else if let Some(i) = find_esc(self.source, self.esc) {
300                    let (txt, source) = self.source.split_at(i);
301                    self.source = source;
302                    return Some(AnsiTxt {
303                        txt,
304                        style: self.style.clone(),
305                    });
306                } else {
307                    return Some(AnsiTxt {
308                        txt: std::mem::take(&mut self.source),
309                        style: self.style.clone(),
310                    });
311                }
312            }
313        }
314    }
315
316    impl AnsiStyle {
317        fn set(&mut self, esc_codes: &str) {
318            let mut esc_codes = esc_codes.split(';');
319            while let Some(code) = esc_codes.next() {
320                match code {
321                    "0" => *self = Self::default(),
322                    "1" => self.weight = AnsiWeight::Bold,
323                    "2" => self.weight = AnsiWeight::Faint,
324                    "3" => self.italic = true,
325                    "4" => self.underline = true,
326                    "5" => self.blink = true,
327                    "7" => self.invert_color = true,
328                    "8" => self.hidden = true,
329                    "9" => self.strikethrough = true,
330                    "22" => self.weight = AnsiWeight::Normal,
331                    "23" => self.italic = false,
332                    "24" => self.underline = false,
333                    "25" => self.blink = false,
334                    "27" => self.invert_color = false,
335                    "28" => self.hidden = false,
336                    "29" => self.strikethrough = false,
337                    "30" => self.color = AnsiColor::Black,
338                    "31" => self.color = AnsiColor::Red,
339                    "32" => self.color = AnsiColor::Green,
340                    "33" => self.color = AnsiColor::Yellow,
341                    "34" => self.color = AnsiColor::Blue,
342                    "35" => self.color = AnsiColor::Magenta,
343                    "36" => self.color = AnsiColor::Cyan,
344                    "37" => self.color = AnsiColor::White,
345                    "40" => self.color = AnsiColor::Black,
346                    "41" => self.color = AnsiColor::Red,
347                    "42" => self.color = AnsiColor::Green,
348                    "43" => self.color = AnsiColor::Yellow,
349                    "44" => self.color = AnsiColor::Blue,
350                    "45" => self.color = AnsiColor::Magenta,
351                    "46" => self.color = AnsiColor::Cyan,
352                    "47" => self.color = AnsiColor::White,
353                    "90" => self.color = AnsiColor::BrightBlack,
354                    "91" => self.color = AnsiColor::BrightRed,
355                    "92" => self.color = AnsiColor::BrightGreen,
356                    "93" => self.color = AnsiColor::BrightYellow,
357                    "94" => self.color = AnsiColor::BrightBlue,
358                    "95" => self.color = AnsiColor::BrightMagenta,
359                    "96" => self.color = AnsiColor::BrightCyan,
360                    "97" => self.color = AnsiColor::BrightWhite,
361                    "100" => self.background_color = AnsiColor::BrightBlack,
362                    "101" => self.background_color = AnsiColor::BrightRed,
363                    "102" => self.background_color = AnsiColor::BrightGreen,
364                    "103" => self.background_color = AnsiColor::BrightYellow,
365                    "104" => self.background_color = AnsiColor::BrightBlue,
366                    "105" => self.background_color = AnsiColor::BrightMagenta,
367                    "106" => self.background_color = AnsiColor::BrightCyan,
368                    "107" => self.background_color = AnsiColor::BrightWhite,
369                    "38" | "48" => {
370                        let target = if code == "38" {
371                            &mut self.color
372                        } else {
373                            &mut self.background_color
374                        };
375                        match esc_codes.next() {
376                            Some("5") => {
377                                let c = esc_codes.next().and_then(|c| c.parse().ok()).unwrap_or(0);
378                                *target = AnsiColor::Ansi256(c)
379                            }
380                            Some("2") => {
381                                let r = esc_codes.next().and_then(|c| c.parse().ok()).unwrap_or(0);
382                                let g = esc_codes.next().and_then(|c| c.parse().ok()).unwrap_or(0);
383                                let b = esc_codes.next().and_then(|c| c.parse().ok()).unwrap_or(0);
384
385                                *target = AnsiColor::TrueColor(r, g, b);
386                            }
387                            _ => {}
388                        }
389                    }
390                    _ => (),
391                }
392            }
393        }
394    }
395}
396
397pub use ansi_fn::*;
398mod ansi_fn {
399    use std::time::Duration;
400
401    use super::{AnsiColor, AnsiStyle, AnsiWeight};
402
403    use super::*;
404
405    /// Arguments for a widget function for an ANSI styled text fragment.
406    ///
407    /// See [`TEXT_FN_VAR`] for more details.
408    #[non_exhaustive]
409    pub struct TextFnArgs {
410        /// The text.
411        pub txt: Txt,
412        /// The ANSI style.
413        pub style: AnsiStyle,
414    }
415    impl TextFnArgs {
416        /// New from text and style.
417        pub fn new(txt: impl Into<Txt>, style: AnsiStyle) -> Self {
418            Self { txt: txt.into(), style }
419        }
420    }
421
422    /// Arguments for a widget function for a text line.
423    ///
424    /// See [`LINE_FN_VAR`] for more details.
425    #[non_exhaustive]
426    pub struct LineFnArgs {
427        /// Zero-counted global index of this line.
428        pub index: u32,
429        /// Zero-counted index of this line in the parent page.
430        pub page_index: u32,
431        /// Text segment widgets, generated by [`TEXT_FN_VAR`].
432        pub text: UiVec,
433    }
434
435    impl LineFnArgs {
436        /// New args.
437        pub fn new(index: u32, page_index: u32, text: UiVec) -> Self {
438            Self { index, page_index, text }
439        }
440    }
441
442    /// Arguments for a widget function for a stack of lines.
443    ///
444    /// See [`PAGE_FN_VAR`] for more details.
445    #[non_exhaustive]
446    pub struct PageFnArgs {
447        /// Zero-counted index of this page.
448        pub index: u32,
449
450        /// Line widgets, generated by [`LINE_FN_VAR`].
451        pub lines: UiVec,
452    }
453
454    impl PageFnArgs {
455        /// New args.
456        pub fn new(index: u32, lines: UiVec) -> Self {
457            Self { index, lines }
458        }
459    }
460
461    /// Arguments for a widget function for a stack of pages.
462    ///
463    /// See [`PANEL_FN_VAR`] for more details.
464    #[non_exhaustive]
465    pub struct PanelFnArgs {
466        /// Page widgets, generated by [`PAGE_FN_VAR`].
467        pub pages: UiVec,
468    }
469
470    impl PanelFnArgs {
471        /// New args.
472        pub fn new(pages: UiVec) -> Self {
473            Self { pages }
474        }
475    }
476
477    context_var! {
478        /// Widget function for [`TextFnArgs`].
479        ///
480        /// The returned widgets are layout by the [`LINE_FN_VAR`]. The default view is [`default_text_fn`].
481        pub static TEXT_FN_VAR: WidgetFn<TextFnArgs> = wgt_fn!(|args: TextFnArgs| { default_text_fn(args) });
482
483        /// Widget function for [`LineFnArgs`].
484        ///
485        /// The returned widgets are layout by the [`PAGE_FN_VAR`]. The default view is [`default_line_fn`].
486        pub static LINE_FN_VAR: WidgetFn<LineFnArgs> = wgt_fn!(|args: LineFnArgs| { default_line_fn(args) });
487
488        /// Widget function for [`PageFnArgs`].
489        ///
490        /// The returned widgets are layout by the [`PANEL_FN_VAR`] widget. The default view is [`default_page_fn`].
491        pub static PAGE_FN_VAR: WidgetFn<PageFnArgs> = wgt_fn!(|args: PageFnArgs| { default_page_fn(args) });
492
493        /// Widget function for [`PanelFnArgs`].
494        ///
495        /// The returned view is the [`AnsiText!`] child. The default is [`default_panel_fn`].
496        ///
497        /// [`AnsiText!`]: struct@super::AnsiText
498        pub static PANEL_FN_VAR: WidgetFn<PanelFnArgs> = wgt_fn!(|args: PanelFnArgs| { default_panel_fn(args) });
499
500        /// Duration the ANSI blink animation keeps the text visible for.
501        ///
502        /// Set to `ZERO` or `MAX` to disable animation.
503        pub static BLINK_INTERVAL_VAR: Duration = Duration::ZERO;
504
505        /// Maximum number of lines per [`PAGE_FN_VAR`].
506        ///
507        /// Is `200` by default.
508        pub static LINES_PER_PAGE_VAR: u32 = 200;
509
510        /// Alternate prefix that is parsed as `'\x1b'` when it marks the start of an ANSI sequence.
511        ///
512        /// Is `r"\x1b"` by default.
513        pub static ALTERNATE_ESC_VAR: Txt = r"\x1b";
514    }
515
516    /// Default [`TEXT_FN_VAR`].
517    ///
518    /// This view is configured by contextual variables like [`BLINK_INTERVAL_VAR`] and all text variables that are
519    /// not overridden by the ANSI style, like the font.
520    ///
521    /// Returns a `Text!` with the text and style.
522    pub fn default_text_fn(args: TextFnArgs) -> UiNode {
523        let mut text = Text::widget_new();
524
525        widget_set! {
526            &mut text;
527            txt = args.txt;
528        }
529
530        if args.style.background_color != AnsiColor::Black {
531            widget_set! {
532                &mut text;
533                background_color = args.style.background_color;
534            }
535        }
536        if args.style.color != AnsiColor::White {
537            widget_set! {
538                &mut text;
539                font_color = args.style.color;
540            }
541        }
542
543        if args.style.weight != AnsiWeight::Normal {
544            widget_set! {
545                &mut text;
546                font_weight = args.style.weight;
547            }
548        }
549        if args.style.italic {
550            widget_set! {
551                &mut text;
552                font_style = FontStyle::Italic;
553            }
554        }
555
556        if args.style.underline {
557            widget_set! {
558                &mut text;
559                underline = 1, LineStyle::Solid;
560            }
561        }
562        if args.style.strikethrough {
563            widget_set! {
564                &mut text;
565                strikethrough = 1, LineStyle::Solid;
566            }
567        }
568
569        if args.style.invert_color {
570            widget_set! {
571                &mut text;
572                invert_color = true;
573            }
574        }
575
576        if args.style.hidden {
577            widget_set! {
578                &mut text;
579                visibility = Visibility::Hidden;
580            }
581        }
582        if args.style.blink && !args.style.hidden {
583            let opacity = var(1.fct());
584
585            let interval = BLINK_INTERVAL_VAR.get();
586            if interval != Duration::ZERO && interval != Duration::MAX {
587                opacity.step_oci(0.fct(), interval).perm();
588
589                widget_set! {
590                    &mut text;
591                    opacity;
592                }
593            }
594        }
595
596        text.widget_build()
597    }
598
599    /// Default [`LINE_FN_VAR`].
600    ///
601    /// Returns a `Wrap!` for text with multiple segments, or returns the single segment, or an empty text.
602    pub fn default_line_fn(mut args: LineFnArgs) -> UiNode {
603        if args.text.is_empty() {
604            Text!("")
605        } else if args.text.len() == 1 {
606            args.text.remove(0)
607        } else {
608            Stack! {
609                rich_text = true;
610                direction = StackDirection::start_to_end();
611                children = args.text;
612            }
613        }
614    }
615
616    /// Default [`PAGE_FN_VAR`].
617    ///
618    /// Returns a `Stack!` for multiple lines, or return the single line, or a nil node.
619    pub fn default_page_fn(mut args: PageFnArgs) -> UiNode {
620        use crate::prelude::*;
621
622        if args.lines.is_empty() {
623            UiNode::nil()
624        } else if args.lines.len() == 1 {
625            args.lines.remove(0)
626        } else {
627            let len = args.lines.len();
628            Stack! {
629                rich_text = true;
630                direction = StackDirection::top_to_bottom();
631                children = args.lines;
632                lazy = LazyMode::lazy_vertical(wgt_fn!(|_| {
633                    let height_sample = zng_wgt_text::node::line_placeholder(50);
634                    zng_wgt_stack::lazy_sample(len, StackDirection::top_to_bottom(), 0, height_sample)
635                }));
636            }
637        }
638    }
639
640    /// Default [`PANEL_FN_VAR`].
641    ///
642    /// Returns a `Stack!` for multiple pages, or returns the single page, or a nil node.
643    pub fn default_panel_fn(mut args: PanelFnArgs) -> UiNode {
644        use crate::prelude::*;
645
646        if args.pages.is_empty() {
647            UiNode::nil()
648        } else if args.pages.len() == 1 {
649            args.pages.remove(0)
650        } else {
651            Stack! {
652                rich_text = true;
653                direction = StackDirection::top_to_bottom();
654                children = args.pages;
655            }
656        }
657    }
658
659    /// ANSI blink animation interval.
660    ///
661    /// Set to `ZERO` to disable the blink animation.
662    ///
663    /// Sets the [`BLINK_INTERVAL_VAR`].
664    #[property(CONTEXT, default(BLINK_INTERVAL_VAR), widget_impl(AnsiText))]
665    pub fn blink_interval(child: impl IntoUiNode, interval: impl IntoVar<Duration>) -> UiNode {
666        with_context_var(child, BLINK_INTERVAL_VAR, interval)
667    }
668
669    /// Widget function that converts [`TextFnArgs`] to widgets.
670    ///
671    /// Sets the [`TEXT_FN_VAR`].
672    #[property(CONTEXT, default(TEXT_FN_VAR), widget_impl(AnsiText))]
673    pub fn text_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<TextFnArgs>>) -> UiNode {
674        with_context_var(child, TEXT_FN_VAR, wgt_fn)
675    }
676
677    /// Widget function that converts [`LineFnArgs`] to widgets.
678    ///
679    /// Sets the [`LINE_FN_VAR`].
680    #[property(CONTEXT, default(LINE_FN_VAR), widget_impl(AnsiText))]
681    pub fn line_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<LineFnArgs>>) -> UiNode {
682        with_context_var(child, LINE_FN_VAR, wgt_fn)
683    }
684
685    /// Widget function that converts [`PageFnArgs`] to widgets.
686    ///
687    /// A *page* is a stack of a maximum of [`lines_per_page`], the text is split in pages mostly for performance reasons.
688    ///
689    /// Sets the [`PAGE_FN_VAR`].
690    ///
691    /// [`lines_per_page`]: fn@lines_per_page
692    #[property(CONTEXT, default(PAGE_FN_VAR), widget_impl(AnsiText))]
693    pub fn page_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<PageFnArgs>>) -> UiNode {
694        with_context_var(child, PAGE_FN_VAR, wgt_fn)
695    }
696
697    /// Widget function that converts [`PanelFnArgs`] to widgets.
698    #[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(AnsiText))]
699    pub fn panel_fn(child: impl IntoUiNode, wgt_fn: impl IntoVar<WidgetFn<PanelFnArgs>>) -> UiNode {
700        with_context_var(child, PANEL_FN_VAR, wgt_fn)
701    }
702
703    /// Maximum number of lines per page view.
704    ///
705    /// Sets the [`LINES_PER_PAGE_VAR`].
706    #[property(CONTEXT, default(LINES_PER_PAGE_VAR), widget_impl(AnsiText))]
707    pub fn lines_per_page(child: impl IntoUiNode, count: impl IntoVar<u32>) -> UiNode {
708        with_context_var(child, LINES_PER_PAGE_VAR, count)
709    }
710    /// Alternate prefix that is parsed as `'\x1b'` when it marks the start of an ANSI sequence.
711    ///
712    /// This is useful for input that comes from a text file for example, where there is no easy
713    /// way to type or edit the ESC character.
714    ///
715    /// An empty text here represents no alternate ESC.
716    ///
717    /// Is `r"\x1b"` by default.
718    ///
719    /// This property sets the [`ALTERNATE_ESC_VAR`].
720    #[property(CONTEXT, default(ALTERNATE_ESC_VAR))]
721    fn alternate_esc(child: impl IntoUiNode, esc: impl IntoVar<Txt>) -> UiNode {
722        with_context_var(child, ALTERNATE_ESC_VAR, esc)
723    }
724}
725
726fn generate_ansi(txt: &Var<Txt>) -> UiNode {
727    use ansi_fn::*;
728    use std::mem;
729
730    txt.with(|txt| {
731        let text_fn = TEXT_FN_VAR.get();
732        let line_fn = LINE_FN_VAR.get();
733        let page_fn = PAGE_FN_VAR.get();
734        let panel_fn = PANEL_FN_VAR.get();
735        let lines_per_page = LINES_PER_PAGE_VAR.get() as usize;
736        let alt_esc = ALTERNATE_ESC_VAR.get();
737
738        let mut pages = Vec::with_capacity(4);
739        let mut lines = Vec::with_capacity(50);
740
741        for (i, line) in txt.lines().enumerate() {
742            let text = ansi_parse::AnsiTextParser::with_esc(line, &["\x1b", alt_esc.as_str()])
743                .filter_map(|txt| {
744                    text_fn.call_checked(TextFnArgs {
745                        txt: txt.txt.to_txt(),
746                        style: txt.style,
747                    })
748                })
749                .collect();
750
751            lines.push(line_fn(LineFnArgs {
752                index: i as u32,
753                page_index: lines.len() as u32,
754                text,
755            }));
756
757            if lines.len() == lines_per_page {
758                let lines = mem::replace(&mut lines, Vec::with_capacity(50));
759                pages.push(page_fn(PageFnArgs {
760                    index: pages.len() as u32,
761                    lines: lines.into(),
762                }));
763            }
764        }
765
766        if !lines.is_empty() {
767            pages.push(page_fn(PageFnArgs {
768                index: pages.len() as u32,
769                lines: lines.into(),
770            }));
771        }
772
773        panel_fn(PanelFnArgs { pages: pages.into() })
774    })
775}
776
777/// Implements the ANSI parsing and view generation, configured by contextual properties.
778pub fn ansi_node(txt: impl IntoVar<Txt>) -> UiNode {
779    let txt = txt.into_var();
780    match_node(UiNode::nil(), move |c, op| match op {
781        UiNodeOp::Init => {
782            WIDGET
783                .sub_var(&txt)
784                .sub_var(&TEXT_FN_VAR)
785                .sub_var(&LINE_FN_VAR)
786                .sub_var(&PAGE_FN_VAR)
787                .sub_var(&PANEL_FN_VAR)
788                .sub_var(&LINES_PER_PAGE_VAR)
789                .sub_var(&BLINK_INTERVAL_VAR)
790                .sub_var(&ALTERNATE_ESC_VAR);
791
792            *c.node() = generate_ansi(&txt);
793        }
794        UiNodeOp::Deinit => {
795            c.deinit();
796            *c.node() = UiNode::nil();
797        }
798        UiNodeOp::Update { .. } => {
799            use ansi_fn::*;
800
801            if txt.is_new()
802                || TEXT_FN_VAR.is_new()
803                || LINE_FN_VAR.is_new()
804                || PAGE_FN_VAR.is_new()
805                || PANEL_FN_VAR.is_new()
806                || LINES_PER_PAGE_VAR.is_new()
807                || BLINK_INTERVAL_VAR.is_new()
808            {
809                c.node().deinit();
810                *c.node() = generate_ansi(&txt);
811                c.node().init();
812                WIDGET.update_info().layout().render();
813            }
814        }
815        _ => {}
816    })
817}
818
819/// Context menu set by the [`AnsiText!`] when [`txt_selectable`] is `true`.
820///
821/// [`AnsiText!`]: struct@AnsiText
822/// [`txt_selectable`]: fn@zng_wgt_text::txt_selectable
823pub fn default_context_menu(args: zng_wgt_menu::context::ContextMenuArgs) -> UiNode {
824    use zng_wgt_button::Button;
825    let id = args.anchor_id;
826    zng_wgt_menu::context::ContextMenu!(ui_vec![
827        Button!(zng_ext_clipboard::COPY_CMD.scoped(id)),
828        Button!(zng_wgt_text::cmd::SELECT_ALL_CMD.scoped(id)),
829    ])
830}
831
832/// Context captured for the context menu, set by the [`AnsiText!`].
833///
834/// Captures all context vars, except text style vars.
835///
836/// [`AnsiText!`]: struct@AnsiText
837pub fn default_popup_context_capture() -> popup::ContextCapture {
838    popup::ContextCapture::context_vars_except(Text::context_vars_set_except_lang)
839}
840
841static X_TERM_256: [(u8, u8, u8); 256] = [
842    (0, 0, 0),
843    (128, 0, 0),
844    (0, 128, 0),
845    (128, 128, 0),
846    (0, 0, 128),
847    (128, 0, 128),
848    (0, 128, 128),
849    (192, 192, 192),
850    (128, 128, 128),
851    (255, 0, 0),
852    (0, 255, 0),
853    (255, 255, 0),
854    (0, 0, 255),
855    (255, 0, 255),
856    (0, 255, 255),
857    (255, 255, 255),
858    (0, 0, 0),
859    (0, 0, 95),
860    (0, 0, 135),
861    (0, 0, 175),
862    (0, 0, 215),
863    (0, 0, 255),
864    (0, 95, 0),
865    (0, 95, 95),
866    (0, 95, 135),
867    (0, 95, 175),
868    (0, 95, 215),
869    (0, 95, 255),
870    (0, 135, 0),
871    (0, 135, 95),
872    (0, 135, 135),
873    (0, 135, 175),
874    (0, 135, 215),
875    (0, 135, 255),
876    (0, 175, 0),
877    (0, 175, 95),
878    (0, 175, 135),
879    (0, 175, 175),
880    (0, 175, 215),
881    (0, 175, 255),
882    (0, 215, 0),
883    (0, 215, 95),
884    (0, 215, 135),
885    (0, 215, 175),
886    (0, 215, 215),
887    (0, 215, 255),
888    (0, 255, 0),
889    (0, 255, 95),
890    (0, 255, 135),
891    (0, 255, 175),
892    (0, 255, 215),
893    (0, 255, 255),
894    (95, 0, 0),
895    (95, 0, 95),
896    (95, 0, 135),
897    (95, 0, 175),
898    (95, 0, 215),
899    (95, 0, 255),
900    (95, 95, 0),
901    (95, 95, 95),
902    (95, 95, 135),
903    (95, 95, 175),
904    (95, 95, 215),
905    (95, 95, 255),
906    (95, 135, 0),
907    (95, 135, 95),
908    (95, 135, 135),
909    (95, 135, 175),
910    (95, 135, 215),
911    (95, 135, 255),
912    (95, 175, 0),
913    (95, 175, 95),
914    (95, 175, 135),
915    (95, 175, 175),
916    (95, 175, 215),
917    (95, 175, 255),
918    (95, 215, 0),
919    (95, 215, 95),
920    (95, 215, 135),
921    (95, 215, 175),
922    (95, 215, 215),
923    (95, 215, 255),
924    (95, 255, 0),
925    (95, 255, 95),
926    (95, 255, 135),
927    (95, 255, 175),
928    (95, 255, 215),
929    (95, 255, 255),
930    (135, 0, 0),
931    (135, 0, 95),
932    (135, 0, 135),
933    (135, 0, 175),
934    (135, 0, 215),
935    (135, 0, 255),
936    (135, 95, 0),
937    (135, 95, 95),
938    (135, 95, 135),
939    (135, 95, 175),
940    (135, 95, 215),
941    (135, 95, 255),
942    (135, 135, 0),
943    (135, 135, 95),
944    (135, 135, 135),
945    (135, 135, 175),
946    (135, 135, 215),
947    (135, 135, 255),
948    (135, 175, 0),
949    (135, 175, 95),
950    (135, 175, 135),
951    (135, 175, 175),
952    (135, 175, 215),
953    (135, 175, 255),
954    (135, 215, 0),
955    (135, 215, 95),
956    (135, 215, 135),
957    (135, 215, 175),
958    (135, 215, 215),
959    (135, 215, 255),
960    (135, 255, 0),
961    (135, 255, 95),
962    (135, 255, 135),
963    (135, 255, 175),
964    (135, 255, 215),
965    (135, 255, 255),
966    (175, 0, 0),
967    (175, 0, 95),
968    (175, 0, 135),
969    (175, 0, 175),
970    (175, 0, 215),
971    (175, 0, 255),
972    (175, 95, 0),
973    (175, 95, 95),
974    (175, 95, 135),
975    (175, 95, 175),
976    (175, 95, 215),
977    (175, 95, 255),
978    (175, 135, 0),
979    (175, 135, 95),
980    (175, 135, 135),
981    (175, 135, 175),
982    (175, 135, 215),
983    (175, 135, 255),
984    (175, 175, 0),
985    (175, 175, 95),
986    (175, 175, 135),
987    (175, 175, 175),
988    (175, 175, 215),
989    (175, 175, 255),
990    (175, 215, 0),
991    (175, 215, 95),
992    (175, 215, 135),
993    (175, 215, 175),
994    (175, 215, 215),
995    (175, 215, 255),
996    (175, 255, 0),
997    (175, 255, 95),
998    (175, 255, 135),
999    (175, 255, 175),
1000    (175, 255, 215),
1001    (175, 255, 255),
1002    (215, 0, 0),
1003    (215, 0, 95),
1004    (215, 0, 135),
1005    (215, 0, 175),
1006    (215, 0, 215),
1007    (215, 0, 255),
1008    (215, 95, 0),
1009    (215, 95, 95),
1010    (215, 95, 135),
1011    (215, 95, 175),
1012    (215, 95, 215),
1013    (215, 95, 255),
1014    (215, 135, 0),
1015    (215, 135, 95),
1016    (215, 135, 135),
1017    (215, 135, 175),
1018    (215, 135, 215),
1019    (215, 135, 255),
1020    (215, 175, 0),
1021    (215, 175, 95),
1022    (215, 175, 135),
1023    (215, 175, 175),
1024    (215, 175, 215),
1025    (215, 175, 255),
1026    (215, 215, 0),
1027    (215, 215, 95),
1028    (215, 215, 135),
1029    (215, 215, 175),
1030    (215, 215, 215),
1031    (215, 215, 255),
1032    (215, 255, 0),
1033    (215, 255, 95),
1034    (215, 255, 135),
1035    (215, 255, 175),
1036    (215, 255, 215),
1037    (215, 255, 255),
1038    (255, 0, 0),
1039    (255, 0, 95),
1040    (255, 0, 135),
1041    (255, 0, 175),
1042    (255, 0, 215),
1043    (255, 0, 255),
1044    (255, 95, 0),
1045    (255, 95, 95),
1046    (255, 95, 135),
1047    (255, 95, 175),
1048    (255, 95, 215),
1049    (255, 95, 255),
1050    (255, 135, 0),
1051    (255, 135, 95),
1052    (255, 135, 135),
1053    (255, 135, 175),
1054    (255, 135, 215),
1055    (255, 135, 255),
1056    (255, 175, 0),
1057    (255, 175, 95),
1058    (255, 175, 135),
1059    (255, 175, 175),
1060    (255, 175, 215),
1061    (255, 175, 255),
1062    (255, 215, 0),
1063    (255, 215, 95),
1064    (255, 215, 135),
1065    (255, 215, 175),
1066    (255, 215, 215),
1067    (255, 215, 255),
1068    (255, 255, 0),
1069    (255, 255, 95),
1070    (255, 255, 135),
1071    (255, 255, 175),
1072    (255, 255, 215),
1073    (255, 255, 255),
1074    (8, 8, 8),
1075    (18, 18, 18),
1076    (28, 28, 28),
1077    (38, 38, 38),
1078    (48, 48, 48),
1079    (58, 58, 58),
1080    (68, 68, 68),
1081    (78, 78, 78),
1082    (88, 88, 88),
1083    (98, 98, 98),
1084    (108, 108, 108),
1085    (118, 118, 118),
1086    (128, 128, 128),
1087    (138, 138, 138),
1088    (148, 148, 148),
1089    (158, 158, 158),
1090    (168, 168, 168),
1091    (178, 178, 178),
1092    (188, 188, 188),
1093    (198, 198, 198),
1094    (208, 208, 208),
1095    (218, 218, 218),
1096    (228, 228, 228),
1097    (238, 238, 238),
1098];