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.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.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.overlay_depth += 1;
176 self.modal_active = true;
177 self.modal_focus_start = self.focus_count;
178 f(self);
179 self.modal_focus_count = self.focus_count.saturating_sub(self.modal_focus_start);
180 self.overlay_depth = self.overlay_depth.saturating_sub(1);
181 self.commands.push(Command::EndOverlay);
182 self.last_text_idx = None;
183 self.response_for(interaction_id)
184 }
185
186 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) -> Response {
188 let interaction_id = self.next_interaction_id();
189 self.commands.push(Command::BeginOverlay { modal: false });
190 self.overlay_depth += 1;
191 f(self);
192 self.overlay_depth = self.overlay_depth.saturating_sub(1);
193 self.commands.push(Command::EndOverlay);
194 self.last_text_idx = None;
195 self.response_for(interaction_id)
196 }
197
198 pub fn tooltip(&mut self, text: impl Into<String>) {
206 let tooltip_text = text.into();
207 if tooltip_text.is_empty() {
208 return;
209 }
210 let last_interaction_id = self.interaction_count.saturating_sub(1);
211 let last_response = self.response_for(last_interaction_id);
212 if !last_response.hovered || last_response.rect.width == 0 || last_response.rect.height == 0
213 {
214 return;
215 }
216 let lines = wrap_tooltip_text(&tooltip_text, 38);
217 self.pending_tooltips.push(PendingTooltip {
218 anchor_rect: last_response.rect,
219 lines,
220 });
221 }
222
223 pub(crate) fn emit_pending_tooltips(&mut self) {
224 let tooltips = std::mem::take(&mut self.pending_tooltips);
225 if tooltips.is_empty() {
226 return;
227 }
228 let area_w = self.area_width;
229 let area_h = self.area_height;
230 let surface = self.theme.surface;
231 let border_color = self.theme.border;
232 let text_color = self.theme.surface_text;
233
234 for tooltip in tooltips {
235 let content_w = tooltip
236 .lines
237 .iter()
238 .map(|l| UnicodeWidthStr::width(l.as_str()) as u32)
239 .max()
240 .unwrap_or(0);
241 let box_w = content_w.saturating_add(4).min(area_w);
242 let box_h = (tooltip.lines.len() as u32).saturating_add(4).min(area_h);
243
244 let tooltip_x = tooltip.anchor_rect.x.min(area_w.saturating_sub(box_w));
245 let below_y = tooltip.anchor_rect.bottom();
246 let tooltip_y = if below_y.saturating_add(box_h) <= area_h {
247 below_y
248 } else {
249 tooltip.anchor_rect.y.saturating_sub(box_h)
250 };
251
252 let lines = tooltip.lines;
253 let _ = self.overlay(|ui| {
254 let _ = ui.container().w(area_w).h(area_h).col(|ui| {
255 let _ = ui
256 .container()
257 .ml(tooltip_x)
258 .mt(tooltip_y)
259 .max_w(box_w)
260 .border(Border::Rounded)
261 .border_fg(border_color)
262 .bg(surface)
263 .p(1)
264 .col(|ui| {
265 for line in &lines {
266 ui.text(line.as_str()).fg(text_color);
267 }
268 });
269 });
270 });
271 }
272 }
273
274 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
282 self.group_count = self.group_count.saturating_add(1);
283 self.group_stack.push(name.to_string());
284 self.container().group_name(name.to_string())
285 }
286
287 pub fn container(&mut self) -> ContainerBuilder<'_> {
308 let border = self.theme.border;
309 ContainerBuilder {
310 ctx: self,
311 gap: 0,
312 row_gap: None,
313 col_gap: None,
314 align: Align::Start,
315 align_self_value: None,
316 justify: Justify::Start,
317 border: None,
318 border_sides: BorderSides::all(),
319 border_style: Style::new().fg(border),
320 bg: None,
321 text_color: None,
322 dark_bg: None,
323 dark_border_style: None,
324 group_hover_bg: None,
325 group_hover_border_style: None,
326 group_name: None,
327 padding: Padding::default(),
328 margin: Margin::default(),
329 constraints: Constraints::default(),
330 title: None,
331 grow: 0,
332 scroll_offset: None,
333 }
334 }
335
336 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
355 let index = self.scroll_count;
356 self.scroll_count += 1;
357 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
358 state.set_bounds(ch, vh);
359 let max = ch.saturating_sub(vh) as usize;
360 state.offset = state.offset.min(max);
361 }
362
363 let next_id = self.interaction_count;
364 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
365 let inner_rects: Vec<Rect> = self
366 .prev_scroll_rects
367 .iter()
368 .enumerate()
369 .filter(|&(j, sr)| {
370 j != index
371 && sr.width > 0
372 && sr.height > 0
373 && sr.x >= rect.x
374 && sr.right() <= rect.right()
375 && sr.y >= rect.y
376 && sr.bottom() <= rect.bottom()
377 })
378 .map(|(_, sr)| *sr)
379 .collect();
380 self.auto_scroll_nested(&rect, state, &inner_rects);
381 }
382
383 self.container().scroll_offset(state.offset as u32)
384 }
385
386 pub fn scrollbar(&mut self, state: &ScrollState) {
406 let vh = state.viewport_height();
407 let ch = state.content_height();
408 if vh == 0 || ch <= vh {
409 return;
410 }
411
412 let track_height = vh;
413 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
414 let max_offset = ch.saturating_sub(vh);
415 let thumb_pos = if max_offset == 0 {
416 0
417 } else {
418 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
419 .round() as u32
420 };
421
422 let theme = self.theme;
423 let track_char = '│';
424 let thumb_char = '█';
425
426 let _ = self.container().w(1).h(track_height).col(|ui| {
427 for i in 0..track_height {
428 if i >= thumb_pos && i < thumb_pos + thumb_height {
429 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
430 } else {
431 ui.styled(
432 track_char.to_string(),
433 Style::new().fg(theme.text_dim).dim(),
434 );
435 }
436 }
437 });
438 }
439
440 fn auto_scroll_nested(
441 &mut self,
442 rect: &Rect,
443 state: &mut ScrollState,
444 inner_scroll_rects: &[Rect],
445 ) {
446 let mut to_consume: Vec<usize> = Vec::new();
447
448 for (i, event) in self.events.iter().enumerate() {
449 if self.consumed[i] {
450 continue;
451 }
452 if let Event::Mouse(mouse) = event {
453 let in_bounds = mouse.x >= rect.x
454 && mouse.x < rect.right()
455 && mouse.y >= rect.y
456 && mouse.y < rect.bottom();
457 if !in_bounds {
458 continue;
459 }
460 let in_inner = inner_scroll_rects.iter().any(|sr| {
461 mouse.x >= sr.x
462 && mouse.x < sr.right()
463 && mouse.y >= sr.y
464 && mouse.y < sr.bottom()
465 });
466 if in_inner {
467 continue;
468 }
469 let delta = self.scroll_lines_per_event as usize;
470 match mouse.kind {
471 MouseKind::ScrollUp => {
472 state.scroll_up(delta);
473 to_consume.push(i);
474 }
475 MouseKind::ScrollDown => {
476 state.scroll_down(delta);
477 to_consume.push(i);
478 }
479 MouseKind::Drag(MouseButton::Left) => {}
480 _ => {}
481 }
482 }
483 }
484
485 for i in to_consume {
486 self.consumed[i] = true;
487 }
488 }
489
490 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
494 self.container()
495 .border(border)
496 .border_sides(BorderSides::all())
497 }
498
499 fn push_container(
500 &mut self,
501 direction: Direction,
502 gap: u32,
503 f: impl FnOnce(&mut Context),
504 ) -> Response {
505 let interaction_id = self.next_interaction_id();
506 let border = self.theme.border;
507
508 self.commands.push(Command::BeginContainer {
509 direction,
510 gap,
511 align: Align::Start,
512 align_self: None,
513 justify: Justify::Start,
514 border: None,
515 border_sides: BorderSides::all(),
516 border_style: Style::new().fg(border),
517 bg_color: None,
518 padding: Padding::default(),
519 margin: Margin::default(),
520 constraints: Constraints::default(),
521 title: None,
522 grow: 0,
523 group_name: None,
524 });
525 self.text_color_stack.push(None);
526 f(self);
527 self.text_color_stack.pop();
528 self.commands.push(Command::EndContainer);
529 self.last_text_idx = None;
530
531 self.response_for(interaction_id)
532 }
533
534 pub(crate) fn response_for(&self, interaction_id: usize) -> Response {
535 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
536 return Response::none();
537 }
538 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
539 let clicked = self
540 .click_pos
541 .map(|(mx, my)| {
542 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
543 })
544 .unwrap_or(false);
545 let hovered = self
546 .mouse_pos
547 .map(|(mx, my)| {
548 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
549 })
550 .unwrap_or(false);
551 Response {
552 clicked,
553 hovered,
554 changed: false,
555 focused: false,
556 rect: *rect,
557 }
558 } else {
559 Response::none()
560 }
561 }
562
563 pub fn is_group_hovered(&self, name: &str) -> bool {
565 if let Some(pos) = self.mouse_pos {
566 self.prev_group_rects.iter().any(|(n, rect)| {
567 n == name
568 && pos.0 >= rect.x
569 && pos.0 < rect.x + rect.width
570 && pos.1 >= rect.y
571 && pos.1 < rect.y + rect.height
572 })
573 } else {
574 false
575 }
576 }
577
578 pub fn is_group_focused(&self, name: &str) -> bool {
580 if self.prev_focus_count == 0 {
581 return false;
582 }
583 let focused_index = self.focus_index % self.prev_focus_count;
584 self.prev_focus_groups
585 .get(focused_index)
586 .and_then(|group| group.as_deref())
587 .map(|group| group == name)
588 .unwrap_or(false)
589 }
590
591 pub fn form(
595 &mut self,
596 state: &mut FormState,
597 f: impl FnOnce(&mut Context, &mut FormState),
598 ) -> &mut Self {
599 let _ = self.col(|ui| {
600 f(ui, state);
601 });
602 self
603 }
604
605 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
609 let _ = self.col(|ui| {
610 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
611 let _ = ui.text_input(&mut field.input);
612 if let Some(error) = field.error.as_deref() {
613 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
614 }
615 });
616 self
617 }
618
619 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
623 self.button(label)
624 }
625}