1use colors::{ACCENT_COLOR_VAR, BASE_COLOR_VAR};
2use zng_ext_clipboard::{COPY_CMD, CUT_CMD, PASTE_CMD};
3use zng_wgt::base_color;
4use zng_wgt::{align, is_disabled, margin, prelude::*};
5use zng_wgt_access::{AccessRole, access_role};
6use zng_wgt_button::Button;
7use zng_wgt_data::{DATA, DataNoteLevel, DataNotes};
8use zng_wgt_fill::foreground_highlight;
9use zng_wgt_filter::{child_opacity, saturate};
10use zng_wgt_input::{
11 focus::{focusable, is_return_focus},
12 pointer_capture::capture_pointer,
13};
14use zng_wgt_layer::popup;
15use zng_wgt_menu::{
16 self as menu,
17 context::{ContextMenu, context_menu_fn},
18};
19use zng_wgt_rule_line::hr::Hr;
20use zng_wgt_size_offset::{offset, y};
21use zng_wgt_style::{Style, StyleMix, impl_named_style_fn, impl_style_fn};
22use zng_wgt_text::{self as text, *};
23use zng_wgt_undo::{UndoMix, undo_scope};
24
25#[widget($crate::TextInput { ($txt:expr) => { txt = $txt; }; })]
54pub struct TextInput(StyleMix<UndoMix<Text>>);
55impl TextInput {
56 fn widget_intrinsic(&mut self) {
57 self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
58 widget_set! {
59 self;
60
61 access_role = AccessRole::TextInput;
62 txt_editable = true;
63 txt_selectable = true;
64 capture_pointer = true;
65 txt_align = Align::TOP_START;
66 focusable = true;
67 undo_scope = true;
68 undo_limit = 100;
69 }
70 }
71}
72impl_style_fn!(TextInput, DefaultStyle);
73
74pub fn default_context_menu(args: menu::context::ContextMenuArgs) -> UiNode {
78 let id = args.anchor_id;
79 ContextMenu!(ui_vec![
80 Button!(CUT_CMD.scoped(id)),
81 Button!(COPY_CMD.scoped(id)),
82 Button!(PASTE_CMD.scoped(id)),
83 Hr!(),
84 Button!(text::cmd::SELECT_ALL_CMD.scoped(id)),
85 ])
86}
87
88pub fn default_selection_toolbar(args: text::SelectionToolbarArgs) -> UiNode {
92 if args.is_touch {
93 let id = args.anchor_id;
94 ContextMenu! {
95 style_fn = menu::context::TouchStyle!();
96 children = ui_vec![
97 Button!(CUT_CMD.scoped(id)),
98 Button!(COPY_CMD.scoped(id)),
99 Button!(PASTE_CMD.scoped(id)),
100 Button!(text::cmd::SELECT_ALL_CMD.scoped(id)),
101 ];
102 }
103 } else {
104 UiNode::nil()
105 }
106}
107
108pub fn default_popup_context_capture() -> popup::ContextCapture {
114 popup::ContextCapture::context_vars_except(Text::context_vars_set_except_lang)
115}
116
117#[widget($crate::text_input::DefaultStyle)]
119pub struct DefaultStyle(Style);
120impl DefaultStyle {
121 fn widget_intrinsic(&mut self) {
122 use zng_wgt::border;
123 use zng_wgt_container::*;
124 use zng_wgt_fill::*;
125 use zng_wgt_input::{focus::is_focused, *};
126 use zng_wgt_layer::*;
127
128 widget_set! {
129 self;
130 replace = true;
131 padding = (7, 10);
132 cursor = CursorIcon::Text;
133 base_color = light_dark(rgb(0.88, 0.88, 0.88), rgb(0.12, 0.12, 0.12));
134 background_color = BASE_COLOR_VAR.rgba();
135 border = {
136 widths: 1,
137 sides: BASE_COLOR_VAR.shade_fct_into(0.20),
138 };
139
140 popup::context_capture = default_popup_context_capture();
141 context_menu_fn = WidgetFn::new(default_context_menu);
142 selection_toolbar_fn = WidgetFn::new(default_selection_toolbar);
143 selection_color = ACCENT_COLOR_VAR.rgba_map(|c| c.with_alpha(30.pct()));
144
145 when *#is_cap_hovered || *#is_return_focus {
146 border = {
147 widths: 1,
148 sides: BASE_COLOR_VAR.shade_fct_into(0.30),
149 };
150 }
151
152 when *#is_focused {
153 border = {
154 widths: 1,
155 sides: ACCENT_COLOR_VAR.rgba_into(),
156 };
157 }
158
159 when *#is_disabled {
160 saturate = false;
161 child_opacity = 50.pct();
162 cursor = CursorIcon::NotAllowed;
163 }
164 }
165 }
166}
167
168#[widget($crate::SearchStyle)]
170pub struct SearchStyle(DefaultStyle);
171impl_named_style_fn!(search, SearchStyle);
172impl SearchStyle {
173 fn widget_intrinsic(&mut self) {
174 widget_set! {
175 self;
176 named_style_fn = SEARCH_STYLE_FN_VAR;
177 zng_wgt_container::padding = (7, 10, 7, 0);
178 zng_wgt_access::access_role = zng_wgt_access::AccessRole::SearchBox;
179 auto_selection = true;
180
181 zng_wgt_container::child_out_start = zng_wgt_container::Container! {
182 child = zng_wgt::ICONS.req("search");
183 zng_wgt::align = Align::CENTER;
184 zng_wgt::hit_test_mode = false;
185 zng_wgt_size_offset::size = 18;
186 zng_wgt::margin = DIRECTION_VAR.map(|d| {
187 match d {
188 LayoutDirection::LTR => (0, 0, 0, 6),
189 LayoutDirection::RTL => (0, 6, 0, 0),
190 }
191 .into()
192 });
193 };
194
195 zng_wgt_container::child_out_end = zng_wgt_button::Button! {
196 zng_wgt::corner_radius = 0;
197 style_fn = zng_wgt_button::LightStyle!();
198 child = zng_wgt::ICONS.req("backspace");
199 focusable = false;
200 zng_wgt::visibility = zng_var::contextual_var(|| {
201 zng_wgt_text::node::TEXT.resolved().txt.clone().map(|t| match t.is_empty() {
202 true => Visibility::Collapsed,
203 false => Visibility::Visible,
204 })
205 });
206 on_click = hn!(|args| {
207 args.propagation.stop();
208 zng_wgt_text::cmd::EDIT_CMD
209 .scoped(WIDGET.info().parent().unwrap().id())
210 .notify_param(zng_wgt_text::cmd::TextEditOp::clear());
211 });
212 };
213 }
214 }
215}
216
217#[property(CHILD, default(""), widget_impl(TextInput, DefaultStyle))]
222pub fn placeholder_txt(child: impl IntoUiNode, txt: impl IntoVar<Txt>) -> UiNode {
223 placeholder(
224 child,
225 Text! {
226 txt;
227 zng_wgt_filter::opacity = 50.pct();
228 zng_wgt::hit_test_mode = false;
229 },
230 )
231}
232
233#[property(CHILD, widget_impl(TextInput, DefaultStyle))]
237pub fn placeholder(child: impl IntoUiNode, placeholder: impl IntoUiNode) -> UiNode {
238 let mut txt_is_empty = None;
239 zng_wgt_container::child_under(
240 child,
241 match_widget(placeholder, move |c, op| match op {
242 UiNodeOp::Init => {
243 let is_empty = zng_wgt_text::node::TEXT.resolved().txt.map(|t| t.is_empty());
244 WIDGET.sub_var_render(&is_empty);
245 txt_is_empty = Some(is_empty);
246 }
247 UiNodeOp::Deinit => {
248 txt_is_empty = None;
249 }
250 UiNodeOp::Render { frame } => {
251 c.delegated();
252 if txt_is_empty.as_ref().unwrap().get() {
253 c.render(frame);
254 } else {
255 frame.hide(|frame| c.render(frame));
256 }
257 }
258 UiNodeOp::RenderUpdate { update } => {
259 c.delegated();
260 if txt_is_empty.as_ref().unwrap().get() {
261 c.render_update(update);
262 } else {
263 update.hidden(|update| c.render_update(update));
264 }
265 }
266 _ => {}
267 }),
268 )
269}
270
271#[widget($crate::FieldStyle)]
278pub struct FieldStyle(DefaultStyle);
279impl_named_style_fn!(field, FieldStyle);
280impl FieldStyle {
281 fn widget_intrinsic(&mut self) {
282 let top_notes = var(DataNotes::default());
283
284 let top_level_and_txt = top_notes.map(|ns| {
285 if let Some(n) = ns.first() {
286 return (n.level(), formatx!("{ns}"));
287 }
288 (DataNoteLevel::INFO, "".into())
289 });
290 let top_txt = top_level_and_txt.map(|(_, t)| t.clone());
291 let top_color = DATA.note_color(top_level_and_txt.map(|(l, _)| *l));
292
293 let highlight = top_level_and_txt.map(|(l, _)| *l >= DataNoteLevel::WARN);
294 let adorn = merge_var!(top_txt.clone(), FIELD_HELP_VAR, |t, h| (t.is_empty(), h.is_empty()));
295
296 let chars_count = var(0usize);
297 let has_max_count = MAX_CHARS_COUNT_VAR.map(|&c| c > 0);
298
299 widget_set! {
300 self;
301 named_style_fn = FIELD_STYLE_FN_VAR;
302 zng_wgt_data::get_data_notes_top = top_notes.clone();
303 get_chars_count = chars_count.clone();
304 auto_selection = true;
305
306 foreground_highlight = {
307 offsets: -2, widths: 1,
309 sides: merge_var!(highlight, top_color.clone(), |&h, &c| if h {
310 c.into()
311 } else {
312 BorderSides::hidden()
313 }),
314 };
315 data_notes_adorner_fn = adorn.map(move |&(top_txt_empty, help_empty)| {
316 if !top_txt_empty {
317 wgt_fn!(top_txt, top_color, |_| Text! {
318 focusable = false;
319 txt_editable = false;
320 txt_selectable = false;
321 txt = top_txt.clone();
322 font_color = top_color.clone();
323 font_size = 0.8.em();
324 align = Align::BOTTOM_START;
325 margin = (0, 4);
326 y = 2.dip() + 100.pct();
327 })
328 } else if !help_empty {
329 wgt_fn!(|_| Text! {
330 focusable = false;
331 txt_editable = false;
332 txt_selectable = false;
333 txt = FIELD_HELP_VAR;
334 font_size = 0.8.em();
335 font_color = text::FONT_COLOR_VAR.map(|c| colors::GRAY.with_alpha(10.pct()).mix_normal(*c));
336 align = Align::BOTTOM_START;
337 margin = (0, 4);
338 y = 2.dip() + 100.pct();
339 })
340 } else {
341 WidgetFn::nil()
342 }
343 });
344
345 max_chars_count_adorner_fn = has_max_count.map(move |&has| {
346 if has {
347 wgt_fn!(chars_count, |_| Text! {
348 focusable = false;
349 txt_editable = false;
350 txt_selectable = false;
351 txt = merge_var!(chars_count.clone(), text::MAX_CHARS_COUNT_VAR, |c, m| formatx!(
352 "{c}/{m}"
353 ));
354 font_color = text::FONT_COLOR_VAR.map(|c| colors::GRAY.with_alpha(10.pct()).mix_normal(*c));
355 font_size = 0.8.em();
356 align = Align::BOTTOM_END;
357 offset = (-4, 2.dip() + 100.pct());
358 })
359 } else {
360 WidgetFn::nil()
361 }
362 });
363
364 margin = (0, 0, 1.2.em(), 0);
365 }
366 }
367}
368
369#[property(FILL, default(WidgetFn::nil()))]
373pub fn data_notes_adorner_fn(child: impl IntoUiNode, adorner_fn: impl IntoVar<WidgetFn<()>>) -> UiNode {
374 zng_wgt_layer::adorner_fn(child, adorner_fn)
375}
376
377#[property(FILL, default(WidgetFn::nil()))]
381pub fn max_chars_count_adorner_fn(child: impl IntoUiNode, adorner_fn: impl IntoVar<WidgetFn<()>>) -> UiNode {
382 zng_wgt_layer::adorner_fn(child, adorner_fn)
383}
384
385context_var! {
386 pub static FIELD_HELP_VAR: Txt = "";
390}
391
392#[property(CONTEXT, default(""))]
396pub fn field_help(child: impl IntoUiNode, help: impl IntoVar<Txt>) -> UiNode {
397 with_context_var(child, FIELD_HELP_VAR, help)
398}