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::CaptureBlend {
115 filter: CaptureFilter::ContextVars {
116 exclude: {
117 let mut exclude = ContextValueSet::new();
118 Text::context_vars_set(&mut exclude);
119
120 let mut allow = ContextValueSet::new();
121 LangMix::<()>::context_vars_set(&mut allow);
122 exclude.remove_all(&allow);
123
124 exclude
125 },
126 },
127 over: false,
128 }
129}
130
131#[widget($crate::text_input::DefaultStyle)]
133pub struct DefaultStyle(Style);
134impl DefaultStyle {
135 fn widget_intrinsic(&mut self) {
136 use zng_wgt::border;
137 use zng_wgt_container::*;
138 use zng_wgt_fill::*;
139 use zng_wgt_input::{focus::is_focused, *};
140 use zng_wgt_layer::*;
141
142 widget_set! {
143 self;
144 replace = true;
145 padding = (7, 10);
146 cursor = CursorIcon::Text;
147 base_color = light_dark(rgb(0.88, 0.88, 0.88), rgb(0.12, 0.12, 0.12));
148 background_color = BASE_COLOR_VAR.rgba();
149 border = {
150 widths: 1,
151 sides: BASE_COLOR_VAR.shade_fct_into(0.20),
152 };
153
154 popup::context_capture = default_popup_context_capture();
155 context_menu_fn = WidgetFn::new(default_context_menu);
156 selection_toolbar_fn = WidgetFn::new(default_selection_toolbar);
157 selection_color = ACCENT_COLOR_VAR.rgba_map(|c| c.with_alpha(30.pct()));
158
159 when *#is_cap_hovered || *#is_return_focus {
160 border = {
161 widths: 1,
162 sides: BASE_COLOR_VAR.shade_fct_into(0.30),
163 };
164 }
165
166 when *#is_focused {
167 border = {
168 widths: 1,
169 sides: ACCENT_COLOR_VAR.rgba_into(),
170 };
171 }
172
173 when *#is_disabled {
174 saturate = false;
175 child_opacity = 50.pct();
176 cursor = CursorIcon::NotAllowed;
177 }
178 }
179 }
180}
181
182#[widget($crate::SearchStyle)]
184pub struct SearchStyle(DefaultStyle);
185impl_named_style_fn!(search, SearchStyle);
186impl SearchStyle {
187 fn widget_intrinsic(&mut self) {
188 widget_set! {
189 self;
190 named_style_fn = SEARCH_STYLE_FN_VAR;
191 zng_wgt_container::padding = (7, 10, 7, 0);
192 zng_wgt_access::access_role = zng_wgt_access::AccessRole::SearchBox;
193 auto_selection = true;
194
195 zng_wgt_container::child_out_start = zng_wgt_container::Container! {
196 child = zng_wgt::ICONS.req("search");
197 zng_wgt::align = Align::CENTER;
198 zng_wgt::hit_test_mode = false;
199 zng_wgt_size_offset::size = 18;
200 zng_wgt::margin = DIRECTION_VAR.map(|d| {
201 match d {
202 LayoutDirection::LTR => (0, 0, 0, 6),
203 LayoutDirection::RTL => (0, 6, 0, 0),
204 }
205 .into()
206 });
207 };
208
209 zng_wgt_container::child_out_end = zng_wgt_button::Button! {
210 zng_wgt::corner_radius = 0;
211 style_fn = zng_wgt_button::LightStyle!();
212 child = zng_wgt::ICONS.req("backspace");
213 focusable = false;
214 zng_wgt::visibility = zng_var::contextual_var(|| {
215 zng_wgt_text::node::TEXT.resolved().txt.clone().map(|t| match t.is_empty() {
216 true => Visibility::Collapsed,
217 false => Visibility::Visible,
218 })
219 });
220 on_click = hn!(|args| {
221 args.propagation().stop();
222 zng_wgt_text::cmd::EDIT_CMD
223 .scoped(WIDGET.info().parent().unwrap().id())
224 .notify_param(zng_wgt_text::cmd::TextEditOp::clear());
225 });
226 };
227 }
228 }
229}
230
231#[property(CHILD, default(""), widget_impl(TextInput, DefaultStyle))]
236pub fn placeholder_txt(child: impl IntoUiNode, txt: impl IntoVar<Txt>) -> UiNode {
237 placeholder(
238 child,
239 Text! {
240 txt;
241 zng_wgt_filter::opacity = 50.pct();
242 zng_wgt::hit_test_mode = false;
243 },
244 )
245}
246
247#[property(CHILD, widget_impl(TextInput, DefaultStyle))]
251pub fn placeholder(child: impl IntoUiNode, placeholder: impl IntoUiNode) -> UiNode {
252 let mut txt_is_empty = None;
253 zng_wgt_container::child_under(
254 child,
255 match_widget(placeholder, move |c, op| match op {
256 UiNodeOp::Init => {
257 let is_empty = zng_wgt_text::node::TEXT.resolved().txt.map(|t| t.is_empty());
258 WIDGET.sub_var_render(&is_empty);
259 txt_is_empty = Some(is_empty);
260 }
261 UiNodeOp::Deinit => {
262 txt_is_empty = None;
263 }
264 UiNodeOp::Render { frame } => {
265 c.delegated();
266 if txt_is_empty.as_ref().unwrap().get() {
267 c.render(frame);
268 } else {
269 frame.hide(|frame| c.render(frame));
270 }
271 }
272 UiNodeOp::RenderUpdate { update } => {
273 c.delegated();
274 if txt_is_empty.as_ref().unwrap().get() {
275 c.render_update(update);
276 } else {
277 update.hidden(|update| c.render_update(update));
278 }
279 }
280 _ => {}
281 }),
282 )
283}
284
285#[widget($crate::FieldStyle)]
292pub struct FieldStyle(DefaultStyle);
293impl_named_style_fn!(field, FieldStyle);
294impl FieldStyle {
295 fn widget_intrinsic(&mut self) {
296 let top_notes = var(DataNotes::default());
297
298 let top_level_and_txt = top_notes.map(|ns| {
299 if let Some(n) = ns.first() {
300 return (n.level(), formatx!("{ns}"));
301 }
302 (DataNoteLevel::INFO, "".into())
303 });
304 let top_txt = top_level_and_txt.map(|(_, t)| t.clone());
305 let top_color = DATA.note_color(top_level_and_txt.map(|(l, _)| *l));
306
307 let highlight = top_level_and_txt.map(|(l, _)| *l >= DataNoteLevel::WARN);
308 let adorn = merge_var!(top_txt.clone(), FIELD_HELP_VAR, |t, h| (t.is_empty(), h.is_empty()));
309
310 let chars_count = var(0usize);
311 let has_max_count = MAX_CHARS_COUNT_VAR.map(|&c| c > 0);
312
313 widget_set! {
314 self;
315 named_style_fn = FIELD_STYLE_FN_VAR;
316 zng_wgt_data::get_data_notes_top = top_notes.clone();
317 get_chars_count = chars_count.clone();
318 auto_selection = true;
319
320 foreground_highlight = {
321 offsets: -2, widths: 1,
323 sides: merge_var!(highlight, top_color.clone(), |&h, &c| if h {
324 c.into()
325 } else {
326 BorderSides::hidden()
327 }),
328 };
329 data_notes_adorner_fn = adorn.map(move |&(top_txt_empty, help_empty)| {
330 if !top_txt_empty {
331 wgt_fn!(top_txt, top_color, |_| Text! {
332 focusable = false;
333 txt_editable = false;
334 txt_selectable = false;
335 txt = top_txt.clone();
336 font_color = top_color.clone();
337 font_size = 0.8.em();
338 align = Align::BOTTOM_START;
339 margin = (0, 4);
340 y = 2.dip() + 100.pct();
341 })
342 } else if !help_empty {
343 wgt_fn!(|_| Text! {
344 focusable = false;
345 txt_editable = false;
346 txt_selectable = false;
347 txt = FIELD_HELP_VAR;
348 font_size = 0.8.em();
349 font_color = text::FONT_COLOR_VAR.map(|c| colors::GRAY.with_alpha(10.pct()).mix_normal(*c));
350 align = Align::BOTTOM_START;
351 margin = (0, 4);
352 y = 2.dip() + 100.pct();
353 })
354 } else {
355 WidgetFn::nil()
356 }
357 });
358
359 max_chars_count_adorner_fn = has_max_count.map(move |&has| {
360 if has {
361 wgt_fn!(chars_count, |_| Text! {
362 focusable = false;
363 txt_editable = false;
364 txt_selectable = false;
365 txt = merge_var!(chars_count.clone(), text::MAX_CHARS_COUNT_VAR, |c, m| formatx!(
366 "{c}/{m}"
367 ));
368 font_color = text::FONT_COLOR_VAR.map(|c| colors::GRAY.with_alpha(10.pct()).mix_normal(*c));
369 font_size = 0.8.em();
370 align = Align::BOTTOM_END;
371 offset = (-4, 2.dip() + 100.pct());
372 })
373 } else {
374 WidgetFn::nil()
375 }
376 });
377
378 margin = (0, 0, 1.2.em(), 0);
379 }
380 }
381}
382
383#[property(FILL, default(WidgetFn::nil()))]
387pub fn data_notes_adorner_fn(child: impl IntoUiNode, adorner_fn: impl IntoVar<WidgetFn<()>>) -> UiNode {
388 zng_wgt_layer::adorner_fn(child, adorner_fn)
389}
390
391#[property(FILL, default(WidgetFn::nil()))]
395pub fn max_chars_count_adorner_fn(child: impl IntoUiNode, adorner_fn: impl IntoVar<WidgetFn<()>>) -> UiNode {
396 zng_wgt_layer::adorner_fn(child, adorner_fn)
397}
398
399context_var! {
400 pub static FIELD_HELP_VAR: Txt = "";
404}
405
406#[property(CONTEXT, default(""))]
410pub fn field_help(child: impl IntoUiNode, help: impl IntoVar<Txt>) -> UiNode {
411 with_context_var(child, FIELD_HELP_VAR, help)
412}