Skip to main content

zng_wgt_text_input/
label.rs

1//! Label text.
2
3use zng_app::property_args;
4use zng_ext_input::{focus::FOCUS, gesture::CLICK_EVENT};
5use zng_wgt::prelude::*;
6use zng_wgt_input::gesture::{Mnemonic, get_mnemonic, get_mnemonic_char, mnemonic_txt};
7use zng_wgt_style::{Style, StyleMix, impl_style_fn};
8
9#[doc(hidden)]
10pub use zng_wgt::prelude::formatx as __formatx;
11use zng_wgt_text::node::TEXT;
12
13/// Styleable and focusable read-only text widget.
14///
15/// Optionally can be the label of a [`target`](#method.target) widget, if set the target widget is focused when the label is focused.
16///
17/// # Shorthand
18///
19/// The widget macro supports the shorthand that sets the `txt` and `target` properties.
20///
21/// ```
22/// # zng_wgt::enable_widget_macros!();
23/// # use zng_wgt::prelude::*;
24/// # use zng_wgt_text_input::label::*;
25/// #
26/// # fn main() {
27/// # let _scope = zng_app::APP.minimal();
28/// let label = Label!("txt", "target");
29/// # }
30/// ```
31#[widget($crate::label::Label {
32    ($txt:expr, $target:expr $(,)?) => {
33        txt = $txt;
34        target = $target;
35    };
36    ($txt:literal) => {
37        txt = $crate::label::__formatx!($txt);
38    };
39    ($txt:expr) => {
40        txt = $txt;
41    };
42})]
43pub struct Label(StyleMix<zng_wgt_text::Text>);
44impl Label {
45    fn widget_intrinsic(&mut self) {
46        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
47
48        // replace the txt with one that removes the mnemonic marker
49        self.widget_builder().push_pre_build_action(|wgt| {
50            let mut mnemonic_data = None;
51            if let Some(txt) = wgt.property_mut(property_id!(zng_wgt_text::txt))
52                && !*txt.captured
53            {
54                let t = txt.args.downcast_var::<Txt>(0);
55
56                let mnemonic = var(Mnemonic::None);
57                mnemonic_data = Some((t.clone(), mnemonic.clone()));
58                let t = expr_var! {
59                    let t = #{t};
60                    if let Mnemonic::FromTxt { marker, .. } = #{mnemonic} {
61                        let mut prev_is_marker = false;
62                        for (i, c) in t.char_indices() {
63                            if c == *marker {
64                                prev_is_marker = true;
65                            } else if prev_is_marker && c.is_alphanumeric() {
66                                return formatx!("{}{}", &t[..i - 1], &t[i..]);
67                            }
68                        }
69                    }
70                    t.clone()
71                };
72                *txt.args = property_args!(zng_wgt_text::txt = t);
73            }
74            if let Some((raw_txt, mnemonic)) = mnemonic_data {
75                wgt.push_intrinsic(NestGroup::WIDGET_INNER, "get_mnemonic", move |c| get_mnemonic(c, mnemonic.clone()));
76                wgt.push_intrinsic(NestGroup::CHILD_LAYOUT + 101, "mnemonic_underline", move |c| {
77                    mnemonic_underline_node(c)
78                });
79                wgt.push_intrinsic(NestGroup::CHILD, "mnemonic_txt", move |c| mnemonic_txt(c, raw_txt.clone()));
80            }
81        });
82    }
83}
84impl_style_fn!(Label, DefaultStyle);
85
86/// Default label style.
87#[widget($crate::label::DefaultStyle)]
88pub struct DefaultStyle(Style);
89impl DefaultStyle {
90    fn widget_intrinsic(&mut self) {
91        widget_set! {
92            self;
93            replace = true;
94        }
95    }
96}
97
98/// Defines the widget the label is for.
99///
100/// When the label is pressed the widget or the first focusable child of the widget is focused.
101/// Accessibility metadata is also set so the target widget is marked as *labelled-by* this widget in the view-process.
102#[property(CONTEXT, widget_impl(Label))]
103pub fn target(child: impl IntoUiNode, target: impl IntoVar<WidgetId>) -> UiNode {
104    let target = target.into_var();
105    let mut prev_target = None::<WidgetId>;
106
107    match_node(child, move |c, op| match op {
108        UiNodeOp::Init => {
109            WIDGET.sub_var(&target).sub_event_when(&CLICK_EVENT, |a| a.is_primary());
110        }
111        UiNodeOp::Info { info } => {
112            c.info(info);
113            if let Some(mut a) = info.access() {
114                let target = target.get();
115                prev_target = Some(target);
116                a.set_labels(target);
117            }
118        }
119        UiNodeOp::Update { updates } => {
120            if let Some(id) = target.get_new() {
121                if let Some(id) = prev_target.take() {
122                    UPDATES.update_info(id);
123                }
124                UPDATES.update_info(id);
125                WIDGET.update_info();
126            }
127
128            c.update(updates);
129
130            if CLICK_EVENT.any_update(true, |a| a.is_primary()) {
131                FOCUS.focus_widget_or_enter(target.get(), true, false);
132            }
133        }
134        _ => {}
135    })
136}
137
138context_var! {
139    /// Mnemonic underline.
140    pub static MNEMONIC_UNDERLINE_VAR: bool = false;
141}
142
143/// Only draw underline under active mnemonic char in labels.
144///
145/// When enabled this overrides [`underline_skip`] in labels, only the char defined by [`get_mnemonic_char`] is underlined.
146///
147/// Note that [`underline`] must also be set to a visible state in context.
148///
149/// [`underline_skip`]: fn@zng_wgt_text::underline_skip
150/// [`underline`]: fn@zng_wgt_text::underline
151#[property(CONTEXT, default(MNEMONIC_UNDERLINE_VAR), widget_impl(Label))]
152pub fn mnemonic_underline(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
153    with_context_var(child, MNEMONIC_UNDERLINE_VAR, enabled)
154}
155
156fn mnemonic_underline_node(child: impl IntoUiNode) -> UiNode {
157    let m_char = var(None);
158    let child = get_mnemonic_char(child, m_char.clone());
159
160    match_node(child, move |c, op| match op {
161        UiNodeOp::Init => {
162            WIDGET.sub_var_layout(&MNEMONIC_UNDERLINE_VAR).sub_var_layout(&m_char);
163        }
164        UiNodeOp::Layout { wl, final_size } => {
165            *final_size = c.layout(wl);
166
167            if MNEMONIC_UNDERLINE_VAR.get() {
168                if let Some(c) = m_char.get() {
169                    let r = TEXT.resolved();
170                    let mut ci = None;
171                    // try exact match first, to underline uppercase chars when possible
172                    for (i, tc) in r.segmented_text.text().char_indices() {
173                        if c == tc {
174                            ci = Some(i);
175                            break;
176                        }
177                    }
178                    if ci.is_none() {
179                        // find char
180                        for (i, tc) in r.segmented_text.text().char_indices() {
181                            if c.to_lowercase().eq(tc.to_lowercase()) {
182                                ci = Some(i);
183                                break;
184                            }
185                        }
186                    }
187                    if let Some(i) = ci {
188                        let l = TEXT.laidout();
189                        let start = l.shaped_text.snap_caret_line(i.into());
190                        let mut end = start;
191                        end.index += c.len_utf8();
192                        let u = l.shaped_text.highlight_underlines(start..end, r.segmented_text.text()).collect();
193                        drop(l);
194                        TEXT.set_underlines(u);
195                    } else {
196                        TEXT.set_underlines(vec![]);
197                    }
198                } else {
199                    // force no underline
200                    TEXT.set_underlines(vec![]);
201                }
202            }
203        }
204        _ => {}
205    })
206}