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