slt/context/widgets_display/
layout.rs1use super::*;
2
3impl Context {
4 pub fn screen(&mut self, name: &str, screens: &ScreenState, f: impl FnOnce(&mut Context)) {
6 if screens.current() == name {
7 f(self);
8 }
9 }
10
11 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
27 self.push_container(Direction::Column, 0, f)
28 }
29
30 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
34 self.push_container(Direction::Column, gap, f)
35 }
36
37 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
54 self.push_container(Direction::Row, 0, f)
55 }
56
57 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
61 self.push_container(Direction::Row, gap, f)
62 }
63
64 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
84 let _ = self.push_container(Direction::Row, 0, f);
85 self
86 }
87
88 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
107 let start = self.commands.len();
108 f(self);
109 let has_link = self.commands[start..]
110 .iter()
111 .any(|cmd| matches!(cmd, Command::Link { .. }));
112
113 if has_link {
114 self.commands.insert(
115 start,
116 Command::BeginContainer {
117 direction: Direction::Row,
118 gap: 0,
119 align: Align::Start,
120 align_self: None,
121 justify: Justify::Start,
122 border: None,
123 border_sides: BorderSides::all(),
124 border_style: Style::new(),
125 bg_color: None,
126 padding: Padding::default(),
127 margin: Margin::default(),
128 constraints: Constraints::default(),
129 title: None,
130 grow: 0,
131 group_name: None,
132 },
133 );
134 self.commands.push(Command::EndContainer);
135 self.rollback.last_text_idx = None;
136 return self;
137 }
138
139 let mut segments: Vec<(String, Style)> = Vec::new();
140 for cmd in self.commands.drain(start..) {
141 match cmd {
142 Command::Text { content, style, .. } => {
143 segments.push((content, style));
144 }
145 Command::Link { text, style, .. } => {
146 segments.push((text, style));
149 }
150 _ => {}
151 }
152 }
153 self.commands.push(Command::RichText {
154 segments,
155 wrap: true,
156 align: Align::Start,
157 margin: Margin::default(),
158 constraints: Constraints::default(),
159 });
160 self.rollback.last_text_idx = None;
161 self
162 }
163
164 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) -> Response {
173 let interaction_id = self.next_interaction_id();
174 self.commands.push(Command::BeginOverlay { modal: true });
175 self.rollback.overlay_depth += 1;
176 self.rollback.modal_active = true;
177 self.rollback.modal_focus_start = self.rollback.focus_count;
178 f(self);
179 self.rollback.modal_focus_count = self
180 .rollback
181 .focus_count
182 .saturating_sub(self.rollback.modal_focus_start);
183 self.rollback.overlay_depth = self.rollback.overlay_depth.saturating_sub(1);
184 self.commands.push(Command::EndOverlay);
185 self.rollback.last_text_idx = None;
186 self.response_for(interaction_id)
187 }
188
189 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
191 let interaction_id = self.next_interaction_id();
192 self.commands.push(Command::BeginOverlay { modal: false });
193 self.rollback.overlay_depth += 1;
194 f(self);
195 self.rollback.overlay_depth = self.rollback.overlay_depth.saturating_sub(1);
196 self.commands.push(Command::EndOverlay);
197 self.rollback.last_text_idx = None;
198 self.response_for(interaction_id)
199 }
200
201 pub fn tooltip(&mut self, text: impl Into<String>) {
209 let tooltip_text = text.into();
210 if tooltip_text.is_empty() {
211 return;
212 }
213 let last_interaction_id = self.rollback.interaction_count.saturating_sub(1);
214 let last_response = self.response_for(last_interaction_id);
215 if !last_response.hovered || last_response.rect.width == 0 || last_response.rect.height == 0
216 {
217 return;
218 }
219 let lines = wrap_tooltip_text(&tooltip_text, 38);
220 self.rollback.pending_tooltips.push(PendingTooltip {
221 anchor_rect: last_response.rect,
222 lines,
223 });
224 }
225
226 pub(crate) fn emit_pending_tooltips(&mut self) {
227 let tooltips = std::mem::take(&mut self.rollback.pending_tooltips);
228 if tooltips.is_empty() {
229 return;
230 }
231 let area_w = self.area_width;
232 let area_h = self.area_height;
233 let surface = self.theme.surface;
234 let border_color = self.theme.border;
235 let text_color = self.theme.surface_text;
236
237 for tooltip in tooltips {
238 let content_w = tooltip
239 .lines
240 .iter()
241 .map(|l| UnicodeWidthStr::width(l.as_str()) as u32)
242 .max()
243 .unwrap_or(0);
244 let box_w = content_w.saturating_add(4).min(area_w);
245 let box_h = (tooltip.lines.len() as u32).saturating_add(4).min(area_h);
246
247 let tooltip_x = tooltip.anchor_rect.x.min(area_w.saturating_sub(box_w));
248 let below_y = tooltip.anchor_rect.bottom();
249 let tooltip_y = if below_y.saturating_add(box_h) <= area_h {
250 below_y
251 } else {
252 tooltip.anchor_rect.y.saturating_sub(box_h)
253 };
254
255 let lines = tooltip.lines;
256 let _ = self.overlay(|ui| {
257 let _ = ui.container().w(area_w).h(area_h).col(|ui| {
258 let _ = ui
259 .container()
260 .ml(tooltip_x)
261 .mt(tooltip_y)
262 .max_w(box_w)
263 .border(Border::Rounded)
264 .border_fg(border_color)
265 .bg(surface)
266 .p(1)
267 .col(|ui| {
268 for line in &lines {
269 ui.text(line.as_str()).fg(text_color);
270 }
271 });
272 });
273 });
274 }
275 }
276
277 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
285 self.rollback.group_count = self.rollback.group_count.saturating_add(1);
286 self.rollback.group_stack.push(name.to_string());
287 self.container().group_name(name.to_string())
288 }
289
290 pub fn container(&mut self) -> ContainerBuilder<'_> {
311 let border = self.theme.border;
312 ContainerBuilder {
313 ctx: self,
314 gap: 0,
315 row_gap: None,
316 col_gap: None,
317 align: Align::Start,
318 align_self_value: None,
319 justify: Justify::Start,
320 border: None,
321 border_sides: BorderSides::all(),
322 border_style: Style::new().fg(border),
323 bg: None,
324 text_color: None,
325 dark_bg: None,
326 dark_border_style: None,
327 group_hover_bg: None,
328 group_hover_border_style: None,
329 group_name: None,
330 padding: Padding::default(),
331 margin: Margin::default(),
332 constraints: Constraints::default(),
333 title: None,
334 grow: 0,
335 scroll_offset: None,
336 }
337 }
338
339 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
358 let index = self.rollback.scroll_count;
359 self.rollback.scroll_count += 1;
360 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
361 state.set_bounds(ch, vh);
362 let max = ch.saturating_sub(vh) as usize;
363 state.offset = state.offset.min(max);
364 }
365
366 let next_id = self.rollback.interaction_count;
367 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
368 let inner_rects: Vec<Rect> = self
369 .prev_scroll_rects
370 .iter()
371 .enumerate()
372 .filter(|&(j, sr)| {
373 j != index
374 && sr.width > 0
375 && sr.height > 0
376 && sr.x >= rect.x
377 && sr.right() <= rect.right()
378 && sr.y >= rect.y
379 && sr.bottom() <= rect.bottom()
380 })
381 .map(|(_, sr)| *sr)
382 .collect();
383 self.auto_scroll_nested(&rect, state, &inner_rects);
384 }
385
386 self.container().scroll_offset(state.offset as u32)
387 }
388
389 pub fn scrollbar(&mut self, state: &ScrollState) {
409 let vh = state.viewport_height();
410 let ch = state.content_height();
411 if vh == 0 || ch <= vh {
412 return;
413 }
414
415 let track_height = vh;
416 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
417 let max_offset = ch.saturating_sub(vh);
418 let thumb_pos = if max_offset == 0 {
419 0
420 } else {
421 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
422 .round() as u32
423 };
424
425 let theme = self.theme;
426 let track_char = '│';
427 let thumb_char = '█';
428
429 let _ = self.container().w(1).h(track_height).col(|ui| {
430 for i in 0..track_height {
431 if i >= thumb_pos && i < thumb_pos + thumb_height {
432 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
433 } else {
434 ui.styled(
435 track_char.to_string(),
436 Style::new().fg(theme.text_dim).dim(),
437 );
438 }
439 }
440 });
441 }
442
443 fn auto_scroll_nested(
444 &mut self,
445 rect: &Rect,
446 state: &mut ScrollState,
447 inner_scroll_rects: &[Rect],
448 ) {
449 let mut to_consume = Vec::new();
450 for (i, mouse) in self.mouse_events_in_rect(*rect) {
451 let in_inner = inner_scroll_rects.iter().any(|sr| {
452 mouse.x >= sr.x && mouse.x < sr.right() && mouse.y >= sr.y && mouse.y < sr.bottom()
453 });
454 if in_inner {
455 continue;
456 }
457
458 let delta = self.scroll_lines_per_event as usize;
459 match mouse.kind {
460 MouseKind::ScrollUp => {
461 state.scroll_up(delta);
462 to_consume.push(i);
463 }
464 MouseKind::ScrollDown => {
465 state.scroll_down(delta);
466 to_consume.push(i);
467 }
468 MouseKind::Drag(MouseButton::Left) => {}
469 _ => {}
470 }
471 }
472 self.consume_indices(to_consume);
473 }
474
475 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
479 self.container()
480 .border(border)
481 .border_sides(BorderSides::all())
482 }
483
484 fn push_container(
485 &mut self,
486 direction: Direction,
487 gap: u32,
488 f: impl FnOnce(&mut Context),
489 ) -> Response {
490 let interaction_id = self.next_interaction_id();
491 let border = self.theme.border;
492
493 self.commands.push(Command::BeginContainer {
494 direction,
495 gap,
496 align: Align::Start,
497 align_self: None,
498 justify: Justify::Start,
499 border: None,
500 border_sides: BorderSides::all(),
501 border_style: Style::new().fg(border),
502 bg_color: None,
503 padding: Padding::default(),
504 margin: Margin::default(),
505 constraints: Constraints::default(),
506 title: None,
507 grow: 0,
508 group_name: None,
509 });
510 self.rollback.text_color_stack.push(None);
511 f(self);
512 self.rollback.text_color_stack.pop();
513 self.commands.push(Command::EndContainer);
514 self.rollback.last_text_idx = None;
515
516 self.response_for(interaction_id)
517 }
518
519 pub(crate) fn response_for(&self, interaction_id: usize) -> Response {
520 if (self.rollback.modal_active || self.prev_modal_active)
521 && self.rollback.overlay_depth == 0
522 {
523 return Response::none();
524 }
525 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
526 let clicked = self
527 .click_pos
528 .map(|(mx, my)| {
529 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
530 })
531 .unwrap_or(false);
532 let hovered = self
533 .mouse_pos
534 .map(|(mx, my)| {
535 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
536 })
537 .unwrap_or(false);
538 Response {
539 clicked,
540 hovered,
541 changed: false,
542 focused: false,
543 rect: *rect,
544 }
545 } else {
546 Response::none()
547 }
548 }
549
550 pub fn is_group_hovered(&self, name: &str) -> bool {
552 if let Some(pos) = self.mouse_pos {
553 self.prev_group_rects.iter().any(|(n, rect)| {
554 n == name
555 && pos.0 >= rect.x
556 && pos.0 < rect.x + rect.width
557 && pos.1 >= rect.y
558 && pos.1 < rect.y + rect.height
559 })
560 } else {
561 false
562 }
563 }
564
565 pub fn is_group_focused(&self, name: &str) -> bool {
567 if self.prev_focus_count == 0 {
568 return false;
569 }
570 let focused_index = self.focus_index % self.prev_focus_count;
571 self.prev_focus_groups
572 .get(focused_index)
573 .and_then(|group| group.as_deref())
574 .map(|group| group == name)
575 .unwrap_or(false)
576 }
577
578 pub fn form(
582 &mut self,
583 state: &mut FormState,
584 f: impl FnOnce(&mut Context, &mut FormState),
585 ) -> &mut Self {
586 let _ = self.col(|ui| {
587 f(ui, state);
588 });
589 self
590 }
591
592 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
596 let _ = self.col(|ui| {
597 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
598 let _ = ui.text_input(&mut field.input);
599 if let Some(error) = field.error.as_deref() {
600 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
601 }
602 });
603 self
604 }
605
606 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
610 self.button(label)
611 }
612}