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_style_fn, style_fn};
22use zng_wgt_text::{self as text, *};
23use zng_wgt_undo::{UndoMix, undo_scope};
24
25#[widget($crate::TextInput {
54 ($txt:expr) => {
55 txt = $txt;
56 };
57})]
58pub struct TextInput(StyleMix<UndoMix<Text>>);
59impl TextInput {
60 fn widget_intrinsic(&mut self) {
61 self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
62 widget_set! {
63 self;
64
65 access_role = AccessRole::TextInput;
66 txt_editable = true;
67 txt_selectable = true;
68 capture_pointer = true;
69 txt_align = Align::TOP_START;
70 focusable = true;
71 undo_scope = true;
72 undo_limit = 100;
73 style_base_fn = style_fn!(|_| DefaultStyle!());
74 }
75 }
76}
77impl_style_fn!(TextInput);
78
79pub fn default_context_menu(args: menu::context::ContextMenuArgs) -> impl UiNode {
83 let id = args.anchor_id;
84 ContextMenu!(ui_vec![
85 Button!(CUT_CMD.scoped(id)),
86 Button!(COPY_CMD.scoped(id)),
87 Button!(PASTE_CMD.scoped(id)),
88 Hr!(),
89 Button!(text::cmd::SELECT_ALL_CMD.scoped(id)),
90 ])
91}
92
93pub fn default_selection_toolbar(args: text::SelectionToolbarArgs) -> impl UiNode {
97 if args.is_touch {
98 let id = args.anchor_id;
99 ContextMenu! {
100 style_fn = menu::context::TouchStyle!();
101 children = ui_vec![
102 Button!(CUT_CMD.scoped(id)),
103 Button!(COPY_CMD.scoped(id)),
104 Button!(PASTE_CMD.scoped(id)),
105 Button!(text::cmd::SELECT_ALL_CMD.scoped(id)),
106 ]
107 }
108 .boxed()
109 } else {
110 NilUiNode.boxed()
111 }
112}
113
114pub fn default_popup_context_capture() -> popup::ContextCapture {
120 popup::ContextCapture::CaptureBlend {
121 filter: CaptureFilter::ContextVars {
122 exclude: {
123 let mut exclude = ContextValueSet::new();
124 Text::context_vars_set(&mut exclude);
125
126 let mut allow = ContextValueSet::new();
127 LangMix::<()>::context_vars_set(&mut allow);
128 exclude.remove_all(&allow);
129
130 exclude
131 },
132 },
133 over: false,
134 }
135}
136
137#[widget($crate::text_input::DefaultStyle)]
139pub struct DefaultStyle(Style);
140impl DefaultStyle {
141 fn widget_intrinsic(&mut self) {
142 use zng_wgt::border;
143 use zng_wgt_container::*;
144 use zng_wgt_fill::*;
145 use zng_wgt_input::{focus::is_focused, *};
146 use zng_wgt_layer::*;
147
148 widget_set! {
149 self;
150 replace = true;
151 padding = (7, 10);
152 cursor = CursorIcon::Text;
153 base_color = light_dark(rgb(0.88, 0.88, 0.88), rgb(0.12, 0.12, 0.12));
154 background_color = BASE_COLOR_VAR.rgba();
155 border = {
156 widths: 1,
157 sides: BASE_COLOR_VAR.shade_fct_into(0.20),
158 };
159
160 popup::context_capture = default_popup_context_capture();
161 context_menu_fn = WidgetFn::new(default_context_menu);
162 selection_toolbar_fn = WidgetFn::new(default_selection_toolbar);
163 selection_color = ACCENT_COLOR_VAR.rgba_map(|c| c.with_alpha(30.pct()));
164
165 when *#is_cap_hovered || *#is_return_focus {
166 border = {
167 widths: 1,
168 sides: BASE_COLOR_VAR.shade_fct_into(0.30),
169 };
170 }
171
172 when *#is_focused {
173 border = {
174 widths: 1,
175 sides: ACCENT_COLOR_VAR.rgba_into(),
176 };
177 }
178
179 when *#is_disabled {
180 saturate = false;
181 child_opacity = 50.pct();
182 cursor = CursorIcon::NotAllowed;
183 }
184 }
185 }
186}
187
188#[widget($crate::SearchStyle)]
190pub struct SearchStyle(DefaultStyle);
191impl SearchStyle {
192 fn widget_intrinsic(&mut self) {
193 widget_set! {
194 self;
195 zng_wgt_container::padding = (7, 10, 7, 0);
196 zng_wgt_access::access_role = zng_wgt_access::AccessRole::SearchBox;
197 auto_selection = true;
198
199 zng_wgt_container::child_out_start = zng_wgt_container::Container! {
200 child = zng_wgt::ICONS.req("search");
201 zng_wgt::align = Align::CENTER;
202 zng_wgt::hit_test_mode = false;
203 zng_wgt_size_offset::size = 18;
204 zng_wgt::margin = DIRECTION_VAR.map(|d| match d {
205 LayoutDirection::LTR => (0, 0, 0, 6),
206 LayoutDirection::RTL => (0, 6, 0, 0),
207 }.into());
208 }, 0;
209
210 zng_wgt_container::child_out_end = zng_wgt_button::Button! {
211 zng_wgt::corner_radius = 0;
212 style_fn = zng_wgt_button::LightStyle!();
213 child = zng_wgt::ICONS.req("backspace");
214 focusable = false;
215 zng_wgt::visibility = zng_var::types::ContextualizedVar::new(|| {
216 zng_wgt_text::node::TEXT.resolved().txt.clone().map(|t| match t.is_empty() {
217 true => Visibility::Collapsed,
218 false => Visibility::Visible,
219 })
220 });
221 on_click = hn!(|args: &zng_ext_input::gesture::ClickArgs| {
222 args.propagation().stop();
223 zng_wgt_text::cmd::EDIT_CMD
224 .scoped(WIDGET.info().parent().unwrap().id())
225 .notify_param(zng_wgt_text::cmd::TextEditOp::clear());
226 });
227 }, 0;
228 }
229 }
230}
231
232#[property(CHILD, default(""), widget_impl(TextInput))]
237pub fn placeholder_txt(child: impl UiNode, txt: impl IntoVar<Txt>) -> impl UiNode {
238 placeholder(
239 child,
240 Text! {
241 txt;
242 zng_wgt_filter::opacity = 50.pct();
243 zng_wgt::hit_test_mode = false;
244 },
245 )
246}
247
248#[property(CHILD, widget_impl(TextInput))]
252pub fn placeholder(child: impl UiNode, placeholder: impl UiNode) -> impl UiNode {
253 let mut txt_is_empty = None;
254 zng_wgt_container::child_under(
255 child,
256 match_widget(placeholder, move |c, op| match op {
257 UiNodeOp::Init => {
258 let is_empty = zng_wgt_text::node::TEXT.resolved().txt.map(|t| t.is_empty());
259 WIDGET.sub_var_render(&is_empty);
260 txt_is_empty = Some(is_empty);
261 }
262 UiNodeOp::Deinit => {
263 txt_is_empty = None;
264 }
265 UiNodeOp::Render { frame } => {
266 c.delegated();
267 if txt_is_empty.as_ref().unwrap().get() {
268 c.render(frame);
269 } else {
270 frame.hide(|frame| c.render(frame));
271 }
272 }
273 UiNodeOp::RenderUpdate { update } => {
274 c.delegated();
275 if txt_is_empty.as_ref().unwrap().get() {
276 c.render_update(update);
277 } else {
278 update.hidden(|update| c.render_update(update));
279 }
280 }
281 _ => {}
282 }),
283 )
284}
285
286#[widget($crate::FieldStyle)]
293pub struct FieldStyle(DefaultStyle);
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_ref(|(_, t)| t);
305 let top_color = DATA.note_color(top_level_and_txt.map_ref(|(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 zng_wgt_data::get_data_notes_top = top_notes.clone();
316 get_chars_count = chars_count.clone();
317 auto_selection = true;
318
319 foreground_highlight = {
320 offsets: -2, widths: 1,
322 sides: merge_var!(highlight, top_color.clone(), |&h, &c| if h {
323 c.into()
324 } else {
325 BorderSides::hidden()
326 }),
327 };
328 data_notes_adorner_fn = adorn.map(move |&(top_txt_empty, help_empty)| {
329 if !top_txt_empty {
330 wgt_fn!(top_txt, top_color, |_| Text! {
331 focusable = false;
332 txt_editable = false;
333 txt_selectable = false;
334 txt = top_txt.clone();
335 font_color = top_color.clone();
336 font_size = 0.8.em();
337 align = Align::BOTTOM_START;
338 margin = (0, 4);
339 y = 2.dip() + 100.pct();
340 })
341 } else if !help_empty {
342 wgt_fn!(|_| Text! {
343 focusable = false;
344 txt_editable = false;
345 txt_selectable = false;
346 txt = FIELD_HELP_VAR;
347 font_size = 0.8.em();
348 font_color = text::FONT_COLOR_VAR.map(|c| colors::GRAY.with_alpha(10.pct()).mix_normal(*c));
349 align = Align::BOTTOM_START;
350 margin = (0, 4);
351 y = 2.dip() + 100.pct();
352 })
353 } else {
354 WidgetFn::nil()
355 }
356 });
357
358 max_chars_count_adorner_fn = has_max_count.map(move |&has| {
359 if has {
360 wgt_fn!(chars_count, |_| Text! {
361 focusable = false;
362 txt_editable = false;
363 txt_selectable = false;
364 txt = merge_var!(chars_count.clone(), text::MAX_CHARS_COUNT_VAR, |c, m| formatx!("{c}/{m}"));
365 font_color = text::FONT_COLOR_VAR.map(|c| colors::GRAY.with_alpha(10.pct()).mix_normal(*c));
366 font_size = 0.8.em();
367 align = Align::BOTTOM_END;
368 offset = (-4, 2.dip() + 100.pct());
369 })
370 } else {
371 WidgetFn::nil()
372 }
373 });
374
375 margin = (0, 0, 1.2.em(), 0);
376 }
377 }
378}
379
380#[property(FILL, default(WidgetFn::nil()))]
384pub fn data_notes_adorner_fn(child: impl UiNode, adorner_fn: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
385 zng_wgt_layer::adorner_fn(child, adorner_fn)
386}
387
388#[property(FILL, default(WidgetFn::nil()))]
392pub fn max_chars_count_adorner_fn(child: impl UiNode, adorner_fn: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
393 zng_wgt_layer::adorner_fn(child, adorner_fn)
394}
395
396context_var! {
397 pub static FIELD_HELP_VAR: Txt = "";
401}
402
403#[property(CONTEXT, default(""))]
407pub fn field_help(child: impl UiNode, help: impl IntoVar<Txt>) -> impl UiNode {
408 with_context_var(child, FIELD_HELP_VAR, help)
409}