slt/context/widgets_display.rs
1use super::*;
2
3impl Context {
4 // ── text ──────────────────────────────────────────────────────────
5
6 /// Render a text element. Returns `&mut Self` for style chaining.
7 ///
8 /// # Example
9 ///
10 /// ```no_run
11 /// # slt::run(|ui: &mut slt::Context| {
12 /// use slt::Color;
13 /// ui.text("hello").bold().fg(Color::Cyan);
14 /// # });
15 /// ```
16 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
17 let content = s.into();
18 self.commands.push(Command::Text {
19 content,
20 style: Style::new(),
21 grow: 0,
22 align: Align::Start,
23 wrap: false,
24 margin: Margin::default(),
25 constraints: Constraints::default(),
26 });
27 self.last_text_idx = Some(self.commands.len() - 1);
28 self
29 }
30
31 /// Render a clickable hyperlink.
32 ///
33 /// The link is interactive: clicking it (or pressing Enter/Space when
34 /// focused) opens the URL in the system browser. OSC 8 is also emitted
35 /// for terminals that support native hyperlinks.
36 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
37 let url_str = url.into();
38 let focused = self.register_focusable();
39 let interaction_id = self.interaction_count;
40 self.interaction_count += 1;
41 let response = self.response_for(interaction_id);
42
43 let mut activated = response.clicked;
44 if focused {
45 for (i, event) in self.events.iter().enumerate() {
46 if let Event::Key(key) = event {
47 if key.kind != KeyEventKind::Press {
48 continue;
49 }
50 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
51 activated = true;
52 self.consumed[i] = true;
53 }
54 }
55 }
56 }
57
58 if activated {
59 let _ = open_url(&url_str);
60 }
61
62 let style = if focused {
63 Style::new()
64 .fg(self.theme.primary)
65 .bg(self.theme.surface_hover)
66 .underline()
67 .bold()
68 } else if response.hovered {
69 Style::new()
70 .fg(self.theme.accent)
71 .bg(self.theme.surface_hover)
72 .underline()
73 } else {
74 Style::new().fg(self.theme.primary).underline()
75 };
76
77 self.commands.push(Command::Link {
78 text: text.into(),
79 url: url_str,
80 style,
81 margin: Margin::default(),
82 constraints: Constraints::default(),
83 });
84 self.last_text_idx = Some(self.commands.len() - 1);
85 self
86 }
87
88 /// Render a text element with word-boundary wrapping.
89 ///
90 /// Long lines are broken at word boundaries to fit the container width.
91 /// Style chaining works the same as [`Context::text`].
92 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
93 let content = s.into();
94 self.commands.push(Command::Text {
95 content,
96 style: Style::new(),
97 grow: 0,
98 align: Align::Start,
99 wrap: true,
100 margin: Margin::default(),
101 constraints: Constraints::default(),
102 });
103 self.last_text_idx = Some(self.commands.len() - 1);
104 self
105 }
106
107 // ── style chain (applies to last text) ───────────────────────────
108
109 /// Apply bold to the last rendered text element.
110 pub fn bold(&mut self) -> &mut Self {
111 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
112 self
113 }
114
115 /// Apply dim styling to the last rendered text element.
116 ///
117 /// Also sets the foreground color to the theme's `text_dim` color if no
118 /// explicit foreground has been set.
119 pub fn dim(&mut self) -> &mut Self {
120 let text_dim = self.theme.text_dim;
121 self.modify_last_style(|s| {
122 s.modifiers |= Modifiers::DIM;
123 if s.fg.is_none() {
124 s.fg = Some(text_dim);
125 }
126 });
127 self
128 }
129
130 /// Apply italic to the last rendered text element.
131 pub fn italic(&mut self) -> &mut Self {
132 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
133 self
134 }
135
136 /// Apply underline to the last rendered text element.
137 pub fn underline(&mut self) -> &mut Self {
138 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
139 self
140 }
141
142 /// Apply reverse-video to the last rendered text element.
143 pub fn reversed(&mut self) -> &mut Self {
144 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
145 self
146 }
147
148 /// Apply strikethrough to the last rendered text element.
149 pub fn strikethrough(&mut self) -> &mut Self {
150 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
151 self
152 }
153
154 /// Set the foreground color of the last rendered text element.
155 pub fn fg(&mut self, color: Color) -> &mut Self {
156 self.modify_last_style(|s| s.fg = Some(color));
157 self
158 }
159
160 /// Set the background color of the last rendered text element.
161 pub fn bg(&mut self, color: Color) -> &mut Self {
162 self.modify_last_style(|s| s.bg = Some(color));
163 self
164 }
165
166 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
167 let apply_group_style = self
168 .group_stack
169 .last()
170 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
171 .unwrap_or(false);
172 if apply_group_style {
173 self.modify_last_style(|s| s.fg = Some(color));
174 }
175 self
176 }
177
178 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
179 let apply_group_style = self
180 .group_stack
181 .last()
182 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
183 .unwrap_or(false);
184 if apply_group_style {
185 self.modify_last_style(|s| s.bg = Some(color));
186 }
187 self
188 }
189
190 /// Render a text element with an explicit [`Style`] applied immediately.
191 ///
192 /// Equivalent to calling `text(s)` followed by style-chain methods, but
193 /// more concise when you already have a `Style` value.
194 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
195 self.commands.push(Command::Text {
196 content: s.into(),
197 style,
198 grow: 0,
199 align: Align::Start,
200 wrap: false,
201 margin: Margin::default(),
202 constraints: Constraints::default(),
203 });
204 self.last_text_idx = Some(self.commands.len() - 1);
205 self
206 }
207
208 /// Render a half-block image in the terminal.
209 ///
210 /// Each terminal cell displays two vertical pixels using the `▀` character
211 /// with foreground (upper pixel) and background (lower pixel) colors.
212 ///
213 /// Create a [`HalfBlockImage`] from a file (requires `image` feature):
214 /// ```ignore
215 /// let img = image::open("photo.png").unwrap();
216 /// let half = HalfBlockImage::from_dynamic(&img, 40, 20);
217 /// ui.image(&half);
218 /// ```
219 ///
220 /// Or from raw RGB data (no feature needed):
221 /// ```no_run
222 /// # use slt::{Context, HalfBlockImage};
223 /// # slt::run(|ui: &mut Context| {
224 /// let rgb = vec![255u8; 30 * 20 * 3];
225 /// let half = HalfBlockImage::from_rgb(&rgb, 30, 10);
226 /// ui.image(&half);
227 /// # });
228 /// ```
229 pub fn image(&mut self, img: &HalfBlockImage) {
230 let width = img.width;
231 let height = img.height;
232
233 self.container().w(width).h(height).gap(0).col(|ui| {
234 for row in 0..height {
235 ui.container().gap(0).row(|ui| {
236 for col in 0..width {
237 let idx = (row * width + col) as usize;
238 if let Some(&(upper, lower)) = img.pixels.get(idx) {
239 ui.styled("▀", Style::new().fg(upper).bg(lower));
240 }
241 }
242 });
243 }
244 });
245 }
246
247 /// Render streaming text with a typing cursor indicator.
248 ///
249 /// Displays the accumulated text content. While `streaming` is true,
250 /// shows a blinking cursor (`▌`) at the end.
251 ///
252 /// ```no_run
253 /// # use slt::widgets::StreamingTextState;
254 /// # slt::run(|ui: &mut slt::Context| {
255 /// let mut stream = StreamingTextState::new();
256 /// stream.start();
257 /// stream.push("Hello from ");
258 /// stream.push("the AI!");
259 /// ui.streaming_text(&mut stream);
260 /// # });
261 /// ```
262 pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
263 if state.streaming {
264 state.cursor_tick = state.cursor_tick.wrapping_add(1);
265 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
266 }
267
268 if state.content.is_empty() && state.streaming {
269 let cursor = if state.cursor_visible { "▌" } else { " " };
270 let primary = self.theme.primary;
271 self.text(cursor).fg(primary);
272 return;
273 }
274
275 if !state.content.is_empty() {
276 if state.streaming && state.cursor_visible {
277 self.text_wrap(format!("{}▌", state.content));
278 } else {
279 self.text_wrap(&state.content);
280 }
281 }
282 }
283
284 /// Render a tool approval widget with approve/reject buttons.
285 ///
286 /// Shows the tool name, description, and two action buttons.
287 /// Returns the updated [`ApprovalAction`] each frame.
288 ///
289 /// ```no_run
290 /// # use slt::widgets::{ApprovalAction, ToolApprovalState};
291 /// # slt::run(|ui: &mut slt::Context| {
292 /// let mut tool = ToolApprovalState::new("read_file", "Read contents of config.toml");
293 /// ui.tool_approval(&mut tool);
294 /// if tool.action == ApprovalAction::Approved {
295 /// }
296 /// # });
297 /// ```
298 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
299 let theme = self.theme;
300 self.bordered(Border::Rounded).col(|ui| {
301 ui.row(|ui| {
302 ui.text("⚡").fg(theme.warning);
303 ui.text(&state.tool_name).bold().fg(theme.primary);
304 });
305 ui.text(&state.description).dim();
306
307 if state.action == ApprovalAction::Pending {
308 ui.row(|ui| {
309 if ui.button("✓ Approve") {
310 state.action = ApprovalAction::Approved;
311 }
312 if ui.button("✗ Reject") {
313 state.action = ApprovalAction::Rejected;
314 }
315 });
316 } else {
317 let (label, color) = match state.action {
318 ApprovalAction::Approved => ("✓ Approved", theme.success),
319 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
320 ApprovalAction::Pending => unreachable!(),
321 };
322 ui.text(label).fg(color).bold();
323 }
324 });
325 }
326
327 /// Render a context bar showing active context items with token counts.
328 ///
329 /// Displays a horizontal bar of context sources (files, URLs, etc.)
330 /// with their token counts, useful for AI chat interfaces.
331 ///
332 /// ```no_run
333 /// # use slt::widgets::ContextItem;
334 /// # slt::run(|ui: &mut slt::Context| {
335 /// let items = vec![ContextItem::new("main.rs", 1200), ContextItem::new("lib.rs", 800)];
336 /// ui.context_bar(&items);
337 /// # });
338 /// ```
339 pub fn context_bar(&mut self, items: &[ContextItem]) {
340 if items.is_empty() {
341 return;
342 }
343
344 let theme = self.theme;
345 let total: usize = items.iter().map(|item| item.tokens).sum();
346
347 self.container().row(|ui| {
348 ui.text("📎").dim();
349 for item in items {
350 ui.text(format!(
351 "{} ({})",
352 item.label,
353 format_token_count(item.tokens)
354 ))
355 .fg(theme.secondary);
356 }
357 ui.spacer();
358 ui.text(format!("Σ {}", format_token_count(total))).dim();
359 });
360 }
361
362 /// Enable word-boundary wrapping on the last rendered text element.
363 pub fn wrap(&mut self) -> &mut Self {
364 if let Some(idx) = self.last_text_idx {
365 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
366 *wrap = true;
367 }
368 }
369 self
370 }
371
372 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
373 if let Some(idx) = self.last_text_idx {
374 match &mut self.commands[idx] {
375 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
376 _ => {}
377 }
378 }
379 }
380
381 // ── containers ───────────────────────────────────────────────────
382
383 /// Create a vertical (column) container.
384 ///
385 /// Children are stacked top-to-bottom. Returns a [`Response`] with
386 /// click/hover state for the container area.
387 ///
388 /// # Example
389 ///
390 /// ```no_run
391 /// # slt::run(|ui: &mut slt::Context| {
392 /// ui.col(|ui| {
393 /// ui.text("line one");
394 /// ui.text("line two");
395 /// });
396 /// # });
397 /// ```
398 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
399 self.push_container(Direction::Column, 0, f)
400 }
401
402 /// Create a vertical (column) container with a gap between children.
403 ///
404 /// `gap` is the number of blank rows inserted between each child.
405 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
406 self.push_container(Direction::Column, gap, f)
407 }
408
409 /// Create a horizontal (row) container.
410 ///
411 /// Children are placed left-to-right. Returns a [`Response`] with
412 /// click/hover state for the container area.
413 ///
414 /// # Example
415 ///
416 /// ```no_run
417 /// # slt::run(|ui: &mut slt::Context| {
418 /// ui.row(|ui| {
419 /// ui.text("left");
420 /// ui.spacer();
421 /// ui.text("right");
422 /// });
423 /// # });
424 /// ```
425 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
426 self.push_container(Direction::Row, 0, f)
427 }
428
429 /// Create a horizontal (row) container with a gap between children.
430 ///
431 /// `gap` is the number of blank columns inserted between each child.
432 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
433 self.push_container(Direction::Row, gap, f)
434 }
435
436 /// Render inline text with mixed styles on a single line.
437 ///
438 /// Unlike [`row`](Context::row), `line()` is designed for rich text —
439 /// children are rendered as continuous inline text without gaps.
440 ///
441 /// # Example
442 ///
443 /// ```no_run
444 /// # use slt::Color;
445 /// # slt::run(|ui: &mut slt::Context| {
446 /// ui.line(|ui| {
447 /// ui.text("Status: ");
448 /// ui.text("Online").bold().fg(Color::Green);
449 /// });
450 /// # });
451 /// ```
452 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
453 let _ = self.push_container(Direction::Row, 0, f);
454 self
455 }
456
457 /// Render inline text with mixed styles, wrapping at word boundaries.
458 ///
459 /// Like [`line`](Context::line), but when the combined text exceeds
460 /// the container width it wraps across multiple lines while
461 /// preserving per-segment styles.
462 ///
463 /// # Example
464 ///
465 /// ```no_run
466 /// # use slt::{Color, Style};
467 /// # slt::run(|ui: &mut slt::Context| {
468 /// ui.line_wrap(|ui| {
469 /// ui.text("This is a long ");
470 /// ui.text("important").bold().fg(Color::Red);
471 /// ui.text(" message that wraps across lines");
472 /// });
473 /// # });
474 /// ```
475 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
476 let start = self.commands.len();
477 f(self);
478 let mut segments: Vec<(String, Style)> = Vec::new();
479 for cmd in self.commands.drain(start..) {
480 if let Command::Text { content, style, .. } = cmd {
481 segments.push((content, style));
482 }
483 }
484 self.commands.push(Command::RichText {
485 segments,
486 wrap: true,
487 align: Align::Start,
488 margin: Margin::default(),
489 constraints: Constraints::default(),
490 });
491 self.last_text_idx = None;
492 self
493 }
494
495 /// Render content in a modal overlay with dimmed background.
496 ///
497 /// ```ignore
498 /// ui.modal(|ui| {
499 /// ui.text("Are you sure?");
500 /// if ui.button("OK") { show = false; }
501 /// });
502 /// ```
503 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
504 self.commands.push(Command::BeginOverlay { modal: true });
505 self.overlay_depth += 1;
506 self.modal_active = true;
507 f(self);
508 self.overlay_depth = self.overlay_depth.saturating_sub(1);
509 self.commands.push(Command::EndOverlay);
510 self.last_text_idx = None;
511 }
512
513 /// Render floating content without dimming the background.
514 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
515 self.commands.push(Command::BeginOverlay { modal: false });
516 self.overlay_depth += 1;
517 f(self);
518 self.overlay_depth = self.overlay_depth.saturating_sub(1);
519 self.commands.push(Command::EndOverlay);
520 self.last_text_idx = None;
521 }
522
523 /// Create a named group container for shared hover/focus styling.
524 ///
525 /// ```ignore
526 /// ui.group("card").border(Border::Rounded)
527 /// .group_hover_bg(Color::Indexed(238))
528 /// .col(|ui| { ui.text("Hover anywhere"); });
529 /// ```
530 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
531 self.group_count = self.group_count.saturating_add(1);
532 self.group_stack.push(name.to_string());
533 self.container().group_name(name.to_string())
534 }
535
536 /// Create a container with a fluent builder.
537 ///
538 /// Use this for borders, padding, grow, constraints, and titles. Chain
539 /// configuration methods on the returned [`ContainerBuilder`], then call
540 /// `.col()` or `.row()` to finalize.
541 ///
542 /// # Example
543 ///
544 /// ```no_run
545 /// # slt::run(|ui: &mut slt::Context| {
546 /// use slt::Border;
547 /// ui.container()
548 /// .border(Border::Rounded)
549 /// .pad(1)
550 /// .title("My Panel")
551 /// .col(|ui| {
552 /// ui.text("content");
553 /// });
554 /// # });
555 /// ```
556 pub fn container(&mut self) -> ContainerBuilder<'_> {
557 let border = self.theme.border;
558 ContainerBuilder {
559 ctx: self,
560 gap: 0,
561 align: Align::Start,
562 justify: Justify::Start,
563 border: None,
564 border_sides: BorderSides::all(),
565 border_style: Style::new().fg(border),
566 bg: None,
567 dark_bg: None,
568 dark_border_style: None,
569 group_hover_bg: None,
570 group_hover_border_style: None,
571 group_name: None,
572 padding: Padding::default(),
573 margin: Margin::default(),
574 constraints: Constraints::default(),
575 title: None,
576 grow: 0,
577 scroll_offset: None,
578 }
579 }
580
581 /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
582 ///
583 /// Pass a [`ScrollState`] to persist scroll position across frames. The state
584 /// is updated in-place with the current scroll offset and bounds.
585 ///
586 /// # Example
587 ///
588 /// ```no_run
589 /// # use slt::widgets::ScrollState;
590 /// # slt::run(|ui: &mut slt::Context| {
591 /// let mut scroll = ScrollState::new();
592 /// ui.scrollable(&mut scroll).col(|ui| {
593 /// for i in 0..100 {
594 /// ui.text(format!("Line {i}"));
595 /// }
596 /// });
597 /// # });
598 /// ```
599 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
600 let index = self.scroll_count;
601 self.scroll_count += 1;
602 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
603 state.set_bounds(ch, vh);
604 let max = ch.saturating_sub(vh) as usize;
605 state.offset = state.offset.min(max);
606 }
607
608 let next_id = self.interaction_count;
609 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
610 let inner_rects: Vec<Rect> = self
611 .prev_scroll_rects
612 .iter()
613 .enumerate()
614 .filter(|&(j, sr)| {
615 j != index
616 && sr.width > 0
617 && sr.height > 0
618 && sr.x >= rect.x
619 && sr.right() <= rect.right()
620 && sr.y >= rect.y
621 && sr.bottom() <= rect.bottom()
622 })
623 .map(|(_, sr)| *sr)
624 .collect();
625 self.auto_scroll_nested(&rect, state, &inner_rects);
626 }
627
628 self.container().scroll_offset(state.offset as u32)
629 }
630
631 /// Render a scrollbar track for a [`ScrollState`].
632 ///
633 /// Displays a track (`│`) with a proportional thumb (`█`). The thumb size
634 /// and position are calculated from the scroll state's content height,
635 /// viewport height, and current offset.
636 ///
637 /// Typically placed beside a `scrollable()` container in a `row()`:
638 /// ```no_run
639 /// # use slt::widgets::ScrollState;
640 /// # slt::run(|ui: &mut slt::Context| {
641 /// let mut scroll = ScrollState::new();
642 /// ui.row(|ui| {
643 /// ui.scrollable(&mut scroll).grow(1).col(|ui| {
644 /// for i in 0..100 { ui.text(format!("Line {i}")); }
645 /// });
646 /// ui.scrollbar(&scroll);
647 /// });
648 /// # });
649 /// ```
650 pub fn scrollbar(&mut self, state: &ScrollState) {
651 let vh = state.viewport_height();
652 let ch = state.content_height();
653 if vh == 0 || ch <= vh {
654 return;
655 }
656
657 let track_height = vh;
658 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
659 let max_offset = ch.saturating_sub(vh);
660 let thumb_pos = if max_offset == 0 {
661 0
662 } else {
663 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
664 .round() as u32
665 };
666
667 let theme = self.theme;
668 let track_char = '│';
669 let thumb_char = '█';
670
671 self.container().w(1).h(track_height).col(|ui| {
672 for i in 0..track_height {
673 if i >= thumb_pos && i < thumb_pos + thumb_height {
674 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
675 } else {
676 ui.styled(
677 track_char.to_string(),
678 Style::new().fg(theme.text_dim).dim(),
679 );
680 }
681 }
682 });
683 }
684
685 fn auto_scroll_nested(
686 &mut self,
687 rect: &Rect,
688 state: &mut ScrollState,
689 inner_scroll_rects: &[Rect],
690 ) {
691 let mut to_consume: Vec<usize> = Vec::new();
692
693 for (i, event) in self.events.iter().enumerate() {
694 if self.consumed[i] {
695 continue;
696 }
697 if let Event::Mouse(mouse) = event {
698 let in_bounds = mouse.x >= rect.x
699 && mouse.x < rect.right()
700 && mouse.y >= rect.y
701 && mouse.y < rect.bottom();
702 if !in_bounds {
703 continue;
704 }
705 let in_inner = inner_scroll_rects.iter().any(|sr| {
706 mouse.x >= sr.x
707 && mouse.x < sr.right()
708 && mouse.y >= sr.y
709 && mouse.y < sr.bottom()
710 });
711 if in_inner {
712 continue;
713 }
714 match mouse.kind {
715 MouseKind::ScrollUp => {
716 state.scroll_up(1);
717 to_consume.push(i);
718 }
719 MouseKind::ScrollDown => {
720 state.scroll_down(1);
721 to_consume.push(i);
722 }
723 MouseKind::Drag(MouseButton::Left) => {}
724 _ => {}
725 }
726 }
727 }
728
729 for i in to_consume {
730 self.consumed[i] = true;
731 }
732 }
733
734 /// Shortcut for `container().border(border)`.
735 ///
736 /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
737 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
738 self.container()
739 .border(border)
740 .border_sides(BorderSides::all())
741 }
742
743 fn push_container(
744 &mut self,
745 direction: Direction,
746 gap: u32,
747 f: impl FnOnce(&mut Context),
748 ) -> Response {
749 let interaction_id = self.interaction_count;
750 self.interaction_count += 1;
751 let border = self.theme.border;
752
753 self.commands.push(Command::BeginContainer {
754 direction,
755 gap,
756 align: Align::Start,
757 justify: Justify::Start,
758 border: None,
759 border_sides: BorderSides::all(),
760 border_style: Style::new().fg(border),
761 bg_color: None,
762 padding: Padding::default(),
763 margin: Margin::default(),
764 constraints: Constraints::default(),
765 title: None,
766 grow: 0,
767 group_name: None,
768 });
769 f(self);
770 self.commands.push(Command::EndContainer);
771 self.last_text_idx = None;
772
773 self.response_for(interaction_id)
774 }
775
776 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
777 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
778 return Response::default();
779 }
780 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
781 let clicked = self
782 .click_pos
783 .map(|(mx, my)| {
784 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
785 })
786 .unwrap_or(false);
787 let hovered = self
788 .mouse_pos
789 .map(|(mx, my)| {
790 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
791 })
792 .unwrap_or(false);
793 Response { clicked, hovered }
794 } else {
795 Response::default()
796 }
797 }
798
799 /// Returns true if the named group is currently hovered by the mouse.
800 pub fn is_group_hovered(&self, name: &str) -> bool {
801 if let Some(pos) = self.mouse_pos {
802 self.prev_group_rects.iter().any(|(n, rect)| {
803 n == name
804 && pos.0 >= rect.x
805 && pos.0 < rect.x + rect.width
806 && pos.1 >= rect.y
807 && pos.1 < rect.y + rect.height
808 })
809 } else {
810 false
811 }
812 }
813
814 /// Returns true if the named group contains the currently focused widget.
815 pub fn is_group_focused(&self, name: &str) -> bool {
816 if self.prev_focus_count == 0 {
817 return false;
818 }
819 let focused_index = self.focus_index % self.prev_focus_count;
820 self.prev_focus_groups
821 .get(focused_index)
822 .and_then(|group| group.as_deref())
823 .map(|group| group == name)
824 .unwrap_or(false)
825 }
826
827 /// Set the flex-grow factor of the last rendered text element.
828 ///
829 /// A value of `1` causes the element to expand and fill remaining space
830 /// along the main axis.
831 pub fn grow(&mut self, value: u16) -> &mut Self {
832 if let Some(idx) = self.last_text_idx {
833 if let Command::Text { grow, .. } = &mut self.commands[idx] {
834 *grow = value;
835 }
836 }
837 self
838 }
839
840 /// Set the text alignment of the last rendered text element.
841 pub fn align(&mut self, align: Align) -> &mut Self {
842 if let Some(idx) = self.last_text_idx {
843 if let Command::Text {
844 align: text_align, ..
845 } = &mut self.commands[idx]
846 {
847 *text_align = align;
848 }
849 }
850 self
851 }
852
853 /// Render an invisible spacer that expands to fill available space.
854 ///
855 /// Useful for pushing siblings to opposite ends of a row or column.
856 pub fn spacer(&mut self) -> &mut Self {
857 self.commands.push(Command::Spacer { grow: 1 });
858 self.last_text_idx = None;
859 self
860 }
861
862 /// Render a form that groups input fields vertically.
863 ///
864 /// Use [`Context::form_field`] inside the closure to render each field.
865 pub fn form(
866 &mut self,
867 state: &mut FormState,
868 f: impl FnOnce(&mut Context, &mut FormState),
869 ) -> &mut Self {
870 self.col(|ui| {
871 f(ui, state);
872 });
873 self
874 }
875
876 /// Render a single form field with label and input.
877 ///
878 /// Shows a validation error below the input when present.
879 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
880 self.col(|ui| {
881 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
882 ui.text_input(&mut field.input);
883 if let Some(error) = field.error.as_deref() {
884 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
885 }
886 });
887 self
888 }
889
890 /// Render a submit button.
891 ///
892 /// Returns `true` when the button is clicked or activated.
893 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
894 self.button(label)
895 }
896}