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