1use khora_core::platform::input::KeyCode;
18use khora_core::ui::editor::ui_builder::{FontFamilyHint, InlineEditEvent, Interaction, TextAlign};
19use khora_core::ui::editor::viewport_texture::ViewportTextureHandle;
20use khora_core::ui::editor::UiBuilder;
21use std::collections::HashMap;
22
23use super::theme::AXIS_COLORS_KEY;
24
25fn font_id_for(family: FontFamilyHint, size: f32) -> egui::FontId {
31 match family {
32 FontFamilyHint::Proportional => egui::FontId::proportional(size),
33 FontFamilyHint::Monospace => egui::FontId::monospace(size),
34 FontFamilyHint::Display => {
35 egui::FontId::new(size, egui::FontFamily::Name("display".into()))
36 }
37 FontFamilyHint::Icons => egui::FontId::new(size, egui::FontFamily::Name("icons".into())),
38 }
39}
40
41pub struct EguiUiBuilder<'a> {
43 ui: &'a mut egui::Ui,
44 viewport_textures: &'a HashMap<ViewportTextureHandle, egui::TextureId>,
46 last_response: Option<egui::Response>,
48 clip_stack: Vec<egui::Rect>,
50}
51
52fn map_key(key: KeyCode) -> Option<egui::Key> {
58 Some(match key {
59 KeyCode::ArrowUp => egui::Key::ArrowUp,
60 KeyCode::ArrowDown => egui::Key::ArrowDown,
61 KeyCode::ArrowLeft => egui::Key::ArrowLeft,
62 KeyCode::ArrowRight => egui::Key::ArrowRight,
63 KeyCode::Enter => egui::Key::Enter,
64 KeyCode::Escape => egui::Key::Escape,
65 KeyCode::Tab => egui::Key::Tab,
66 KeyCode::Home => egui::Key::Home,
67 KeyCode::End => egui::Key::End,
68 KeyCode::Delete => egui::Key::Delete,
69 KeyCode::Backspace => egui::Key::Backspace,
70 KeyCode::F2 => egui::Key::F2,
71 _ => return None,
72 })
73}
74
75impl<'a> EguiUiBuilder<'a> {
76 pub fn new(
78 ui: &'a mut egui::Ui,
79 viewport_textures: &'a HashMap<ViewportTextureHandle, egui::TextureId>,
80 ) -> Self {
81 Self {
82 ui,
83 viewport_textures,
84 last_response: None,
85 clip_stack: Vec::new(),
86 }
87 }
88
89 fn overlay_painter(&self) -> egui::Painter {
93 self.ui.ctx().layer_painter(egui::LayerId::new(
94 egui::Order::Foreground,
95 egui::Id::new("khora_overlay"),
96 ))
97 }
98}
99
100fn color_to_egui(c: [f32; 4]) -> egui::Color32 {
101 egui::Color32::from_rgba_unmultiplied(
102 (c[0] * 255.0) as u8,
103 (c[1] * 255.0) as u8,
104 (c[2] * 255.0) as u8,
105 (c[3] * 255.0) as u8,
106 )
107}
108
109impl UiBuilder for EguiUiBuilder<'_> {
110 fn heading(&mut self, text: &str) {
113 self.ui.heading(text);
114 }
115
116 fn label(&mut self, text: &str) {
117 self.ui.label(text);
118 }
119
120 fn colored_label(&mut self, color: [f32; 4], text: &str) {
121 self.ui.colored_label(color_to_egui(color), text);
122 }
123
124 fn small_label(&mut self, text: &str) {
125 self.ui.small(text);
126 }
127
128 fn monospace(&mut self, text: &str) {
129 self.ui.monospace(text);
130 }
131
132 fn button(&mut self, text: &str) -> bool {
135 let r = self.ui.button(text);
136 let clicked = r.clicked();
137 self.last_response = Some(r);
138 clicked
139 }
140
141 fn small_button(&mut self, text: &str) -> bool {
142 let r = self.ui.small_button(text);
143 let clicked = r.clicked();
144 self.last_response = Some(r);
145 clicked
146 }
147
148 fn selectable_label(&mut self, active: bool, text: &str) -> bool {
149 let r = self.ui.selectable_label(active, text);
150 let clicked = r.clicked();
151 self.last_response = Some(r);
152 clicked
153 }
154
155 fn selectable_label_double_clicked(&mut self, active: bool, text: &str) -> bool {
156 let r = self.ui.selectable_label(active, text);
157 let double_clicked = r.double_clicked();
158 self.last_response = Some(r);
159 double_clicked
160 }
161
162 fn checkbox(&mut self, checked: &mut bool, text: &str) -> bool {
163 let r = self.ui.checkbox(checked, text);
164 let changed = r.changed();
165 self.last_response = Some(r);
166 changed
167 }
168
169 fn drag_value_f32(&mut self, label: &str, value: &mut f32, speed: f32) -> bool {
170 let inner = self.ui.horizontal(|ui| {
171 ui.label(label);
172 ui.add(egui::DragValue::new(value).speed(speed))
173 });
174 let changed = inner.inner.changed();
175 self.last_response = Some(inner.inner);
176 changed
177 }
178
179 fn slider_f32(&mut self, label: &str, value: &mut f32, min: f32, max: f32) -> bool {
180 let r = self.ui.add(egui::Slider::new(value, min..=max).text(label));
181 let changed = r.changed();
182 self.last_response = Some(r);
183 changed
184 }
185
186 fn text_edit_singleline(&mut self, text: &mut String) -> bool {
187 let r = self.ui.text_edit_singleline(text);
188 let changed = r.changed();
189 self.last_response = Some(r);
194 changed
195 }
196
197 fn vec3_editor(&mut self, label: &str, value: &mut [f32; 3], speed: f32) -> bool {
198 let axes = self
206 .ui
207 .ctx()
208 .data(|d| d.get_temp::<[egui::Color32; 3]>(egui::Id::new(AXIS_COLORS_KEY)))
209 .unwrap_or([
210 egui::Color32::from_rgb(246, 109, 103),
211 egui::Color32::from_rgb(114, 207, 142),
212 egui::Color32::from_rgb(115, 204, 234),
213 ]);
214
215 let axis_letter = |ui: &mut egui::Ui, ch: &str, color: egui::Color32| {
216 ui.label(
217 egui::RichText::new(ch)
218 .color(color)
219 .strong()
220 .monospace()
221 .size(10.0),
222 );
223 };
224
225 let inner = self.ui.horizontal(|ui| {
226 if !label.is_empty() {
227 ui.label(label);
228 }
229 let mut changed = false;
230 let mut last = None;
231 for (i, ch) in ["X", "Y", "Z"].iter().enumerate() {
232 axis_letter(ui, ch, axes[i]);
233 let r = ui.add(egui::DragValue::new(&mut value[i]).speed(speed));
234 changed |= r.changed();
235 last = Some(r);
236 }
237 (changed, last)
238 });
239 let (changed, last) = inner.inner;
240 self.last_response = last;
243 changed
244 }
245
246 fn color_edit(&mut self, label: &str, color: &mut [f32; 4]) -> bool {
247 let inner = self.ui.horizontal(|ui| {
248 ui.label(label);
249 ui.color_edit_button_rgba_unmultiplied(color)
250 });
251 let changed = inner.inner.changed();
252 self.last_response = Some(inner.inner);
253 changed
254 }
255
256 fn combo_box(
257 &mut self,
258 id_salt: &str,
259 label: &str,
260 current: &mut usize,
261 options: &[&str],
262 ) -> bool {
263 let selected_text = options.get(*current).copied().unwrap_or("");
264 let mut changed = false;
265 let out = egui::ComboBox::new(("khora_combo", id_salt), label)
270 .selected_text(selected_text)
271 .show_ui(self.ui, |ui| {
272 for (i, option) in options.iter().enumerate() {
273 if ui.selectable_label(i == *current, *option).clicked() {
274 *current = i;
275 changed = true;
276 }
277 }
278 });
279 self.last_response = Some(out.response);
280 changed
281 }
282
283 fn horizontal(&mut self, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
286 let vt = self.viewport_textures;
287 self.ui.horizontal(|ui| {
288 let mut nested = EguiUiBuilder::new(ui, vt);
289 f(&mut nested);
290 });
291 }
292
293 fn vertical(&mut self, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
294 let vt = self.viewport_textures;
295 self.ui.vertical(|ui| {
296 let mut nested = EguiUiBuilder::new(ui, vt);
297 f(&mut nested);
298 });
299 }
300
301 fn collapsing(
302 &mut self,
303 header: &str,
304 default_open: bool,
305 f: &mut dyn FnMut(&mut dyn UiBuilder),
306 ) {
307 let vt = self.viewport_textures;
308 egui::CollapsingHeader::new(header)
309 .default_open(default_open)
310 .show(self.ui, |ui| {
311 let mut nested = EguiUiBuilder::new(ui, vt);
312 f(&mut nested);
313 });
314 }
315
316 fn indent(&mut self, id: &str, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
317 let vt = self.viewport_textures;
318 self.ui.indent(id, |ui| {
319 let mut nested = EguiUiBuilder::new(ui, vt);
320 f(&mut nested);
321 });
322 }
323
324 fn scroll_area(&mut self, id: &str, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
325 let vt = self.viewport_textures;
326 egui::ScrollArea::vertical()
327 .id_salt(id)
328 .show(self.ui, |ui| {
329 let mut nested = EguiUiBuilder::new(ui, vt);
330 f(&mut nested);
331 });
332 }
333
334 fn viewport_image(
335 &mut self,
336 handle: ViewportTextureHandle,
337 size: [f32; 2],
338 ) -> Option<[f32; 2]> {
339 let egui_id = self.viewport_textures.get(&handle)?;
340 let image = egui::Image::new(egui::load::SizedTexture::new(
348 *egui_id,
349 egui::vec2(size[0], size[1]),
350 ))
351 .sense(egui::Sense::hover());
352 let response = self.ui.add(image);
353 let min = response.rect.min;
354 self.last_response = Some(response);
355 Some([min.x, min.y])
356 }
357
358 fn separator(&mut self) {
361 self.ui.separator();
362 }
363
364 fn spacing(&mut self, points: f32) {
365 self.ui.add_space(points);
366 }
367
368 fn is_last_item_double_clicked(&self) -> bool {
371 self.last_response
372 .as_ref()
373 .is_some_and(|r| r.double_clicked())
374 }
375
376 fn is_last_item_hovered(&self) -> bool {
377 self.last_response.as_ref().is_some_and(|r| r.hovered())
378 }
379
380 fn is_last_item_enter_pressed(&self) -> bool {
381 self.last_response
382 .as_ref()
383 .is_some_and(|r| r.lost_focus() && self.ui.input(|i| i.key_pressed(egui::Key::Enter)))
384 }
385
386 fn is_last_item_escape_pressed(&self) -> bool {
387 self.last_response
388 .as_ref()
389 .is_some_and(|_| self.ui.input(|i| i.key_pressed(egui::Key::Escape)))
390 }
391
392 fn context_menu_last(&mut self, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
393 if let Some(response) = self.last_response.clone() {
399 let vt = self.viewport_textures;
400 response.context_menu(|ui| {
401 let mut nested = EguiUiBuilder::new(ui, vt);
402 f(&mut nested);
403 });
404 }
405 }
406
407 fn context_menu_panel(&mut self, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
408 let remaining = self.ui.available_size();
412 let min_h = remaining.y.max(40.0);
415 let (id, rect) = self.ui.allocate_space(egui::vec2(remaining.x, min_h));
416 let response = self.ui.interact(rect, id, egui::Sense::click());
417 let vt = self.viewport_textures;
418 response.context_menu(|ui| {
419 let mut nested = EguiUiBuilder::new(ui, vt);
420 f(&mut nested);
421 });
422 }
423
424 fn close_menu(&mut self) {
425 self.ui.close();
426 }
427
428 fn menu_button(&mut self, label: &str, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
429 let vt = self.viewport_textures;
430 self.ui.menu_button(label, |ui| {
431 let mut nested = EguiUiBuilder::new(ui, vt);
432 f(&mut nested);
433 });
434 }
435
436 fn paint_line(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4], thickness: f32) {
437 self.ui.painter().line_segment(
438 [egui::pos2(from[0], from[1]), egui::pos2(to[0], to[1])],
439 egui::Stroke::new(thickness, color_to_egui(color)),
440 );
441 }
442
443 fn paint_rect_filled(&mut self, min: [f32; 2], size: [f32; 2], color: [f32; 4], rounding: f32) {
444 let rect =
445 egui::Rect::from_min_size(egui::pos2(min[0], min[1]), egui::vec2(size[0], size[1]));
446 let corner = egui::CornerRadius::same(rounding.clamp(0.0, 255.0) as u8);
447 self.ui
448 .painter()
449 .rect_filled(rect, corner, color_to_egui(color));
450 }
451
452 fn paint_text(&mut self, pos: [f32; 2], color: [f32; 4], text: &str) {
453 self.ui.painter().text(
454 egui::pos2(pos[0], pos[1]),
455 egui::Align2::LEFT_TOP,
456 text,
457 egui::FontId::proportional(12.0),
458 color_to_egui(color),
459 );
460 }
461
462 fn available_width(&self) -> f32 {
465 self.ui.available_width()
466 }
467
468 fn available_height(&self) -> f32 {
469 self.ui.available_height()
470 }
471
472 fn panel_rect(&self) -> [f32; 4] {
473 let r = self.ui.max_rect();
474 [r.min.x, r.min.y, r.width(), r.height()]
475 }
476
477 fn screen_rect(&self) -> [f32; 4] {
478 let r = self.ui.ctx().content_rect();
479 [r.min.x, r.min.y, r.width(), r.height()]
480 }
481
482 fn paint_rect_stroke(
483 &mut self,
484 min: [f32; 2],
485 size: [f32; 2],
486 color: [f32; 4],
487 rounding: f32,
488 thickness: f32,
489 ) {
490 let rect =
491 egui::Rect::from_min_size(egui::pos2(min[0], min[1]), egui::vec2(size[0], size[1]));
492 let corner = egui::CornerRadius::same(rounding.clamp(0.0, 255.0) as u8);
493 self.ui.painter().rect_stroke(
494 rect,
495 corner,
496 egui::Stroke::new(thickness, color_to_egui(color)),
497 egui::epaint::StrokeKind::Inside,
498 );
499 }
500
501 fn paint_circle_filled(&mut self, center: [f32; 2], radius: f32, color: [f32; 4]) {
502 self.ui.painter().circle_filled(
503 egui::pos2(center[0], center[1]),
504 radius,
505 color_to_egui(color),
506 );
507 }
508
509 fn paint_circle_stroke(
510 &mut self,
511 center: [f32; 2],
512 radius: f32,
513 color: [f32; 4],
514 thickness: f32,
515 ) {
516 self.ui.painter().circle_stroke(
517 egui::pos2(center[0], center[1]),
518 radius,
519 egui::Stroke::new(thickness, color_to_egui(color)),
520 );
521 }
522
523 fn paint_text_styled(
524 &mut self,
525 pos: [f32; 2],
526 text: &str,
527 size: f32,
528 color: [f32; 4],
529 family: FontFamilyHint,
530 align: TextAlign,
531 ) {
532 let egui_align = match align {
533 TextAlign::Left => egui::Align2::LEFT_TOP,
534 TextAlign::Center => egui::Align2::CENTER_TOP,
535 TextAlign::Right => egui::Align2::RIGHT_TOP,
536 };
537 let font_id = font_id_for(family, size);
538 self.ui.painter().text(
539 egui::pos2(pos[0], pos[1]),
540 egui_align,
541 text,
542 font_id,
543 color_to_egui(color),
544 );
545 }
546
547 fn paint_path_filled(&mut self, points: &[[f32; 2]], color: [f32; 4]) {
548 if points.len() < 3 {
549 return;
550 }
551 use egui::epaint::{PathShape, PathStroke};
552 let pts: Vec<egui::Pos2> = points.iter().map(|p| egui::pos2(p[0], p[1])).collect();
553 self.ui.painter().add(egui::Shape::Path(PathShape {
554 points: pts,
555 closed: true,
556 fill: color_to_egui(color),
557 stroke: PathStroke::NONE,
558 }));
559 }
560
561 fn interact_rect(&mut self, id_salt: &str, rect: [f32; 4]) -> Interaction {
562 let r =
563 egui::Rect::from_min_size(egui::pos2(rect[0], rect[1]), egui::vec2(rect[2], rect[3]));
564 let id = self.ui.id().with(("khora_hot", id_salt));
565 let response = self.ui.interact(r, id, egui::Sense::click_and_drag());
566 let interaction = Interaction {
567 hovered: response.hovered(),
568 clicked: response.clicked(),
569 pressed: response.is_pointer_button_down_on(),
570 double_clicked: response.double_clicked(),
571 focused: response.has_focus(),
572 };
573 self.last_response = Some(response);
574 interaction
575 }
576
577 fn key_pressed(&self, key: KeyCode) -> bool {
578 if self.ui.ctx().egui_wants_keyboard_input() {
581 return false;
582 }
583 let Some(egui_key) = map_key(key) else {
584 return false;
585 };
586 self.ui.input(|i| i.key_pressed(egui_key))
587 }
588
589 fn keyboard_captured(&self) -> bool {
590 self.ui.ctx().egui_wants_keyboard_input()
591 }
592
593 fn raw_key_pressed(&self, key: KeyCode) -> bool {
594 let Some(egui_key) = map_key(key) else {
595 return false;
596 };
597 self.ui.input(|i| i.key_pressed(egui_key))
598 }
599
600 fn focus_last_item(&mut self) {
601 if let Some(response) = self.last_response.as_ref() {
602 response.request_focus();
603 }
604 }
605
606 fn push_clip_rect(&mut self, rect: [f32; 4]) {
607 let r =
608 egui::Rect::from_min_size(egui::pos2(rect[0], rect[1]), egui::vec2(rect[2], rect[3]));
609 self.clip_stack.push(self.ui.clip_rect());
612 let clipped = self.ui.clip_rect().intersect(r);
613 self.ui.set_clip_rect(clipped);
614 }
615
616 fn pop_clip_rect(&mut self) {
617 if let Some(previous) = self.clip_stack.pop() {
618 self.ui.set_clip_rect(previous);
619 }
620 }
621
622 fn scroll_delta_in(&self, rect: [f32; 4]) -> f32 {
623 let r =
624 egui::Rect::from_min_size(egui::pos2(rect[0], rect[1]), egui::vec2(rect[2], rect[3]));
625 let inside = self
626 .ui
627 .ctx()
628 .pointer_latest_pos()
629 .is_some_and(|p| r.contains(p));
630 if !inside {
631 return 0.0;
632 }
633 self.ui.input(|i| i.smooth_scroll_delta.y)
634 }
635
636 fn dnd_attach_drag_payload(&mut self, payload: u64) {
637 if let Some(response) = self.last_response.as_ref() {
641 if response.dragged() {
642 response.dnd_set_drag_payload::<u64>(payload);
643 }
644 }
645 }
646
647 fn dnd_take_drop_payload(&mut self) -> Option<u64> {
648 self.last_response
649 .as_ref()
650 .and_then(|r| r.dnd_release_payload::<u64>())
651 .map(|payload| *payload)
652 }
653
654 fn pointer_position(&self) -> Option<[f32; 2]> {
655 self.ui.ctx().pointer_interact_pos().map(|p| [p.x, p.y])
656 }
657
658 fn is_last_item_dragged(&self) -> bool {
659 self.last_response
660 .as_ref()
661 .map(|r| r.dragged())
662 .unwrap_or(false)
663 }
664
665 fn is_drag_active(&self) -> bool {
666 self.ui.ctx().dragged_id().is_some()
667 }
668
669 fn inline_text_field(
670 &mut self,
671 rect: [f32; 4],
672 id_salt: &str,
673 text: &mut String,
674 request_focus: bool,
675 ) -> InlineEditEvent {
676 let r =
677 egui::Rect::from_min_size(egui::pos2(rect[0], rect[1]), egui::vec2(rect[2], rect[3]));
678 let mut child = self.ui.new_child(
679 egui::UiBuilder::new()
680 .max_rect(r)
681 .id_salt(("khora_inline", id_salt))
682 .layout(egui::Layout::top_down(egui::Align::Min)),
683 );
684 let field_id = egui::Id::new(("khora_inline_edit", id_salt));
685 let resp = child.add(
686 egui::TextEdit::singleline(text)
687 .id(field_id)
688 .desired_width(rect[2]),
689 );
690 if request_focus {
691 resp.request_focus();
692 }
693 if resp.lost_focus() {
694 let escaped = child.input(|i| i.key_pressed(egui::Key::Escape));
698 return if escaped {
699 InlineEditEvent::Cancelled
700 } else {
701 InlineEditEvent::Committed
702 };
703 }
704 if resp.changed() {
705 InlineEditEvent::Changed
706 } else {
707 InlineEditEvent::Idle
708 }
709 }
710
711 fn overlay_rect_filled(
712 &mut self,
713 min: [f32; 2],
714 size: [f32; 2],
715 color: [f32; 4],
716 rounding: f32,
717 ) {
718 let painter = self.overlay_painter();
719 let rect =
720 egui::Rect::from_min_size(egui::pos2(min[0], min[1]), egui::vec2(size[0], size[1]));
721 let corner = egui::CornerRadius::same(rounding.clamp(0.0, 255.0) as u8);
722 painter.rect_filled(rect, corner, color_to_egui(color));
723 }
724
725 fn overlay_rect_stroke(
726 &mut self,
727 min: [f32; 2],
728 size: [f32; 2],
729 color: [f32; 4],
730 rounding: f32,
731 thickness: f32,
732 ) {
733 let painter = self.overlay_painter();
734 let rect =
735 egui::Rect::from_min_size(egui::pos2(min[0], min[1]), egui::vec2(size[0], size[1]));
736 let corner = egui::CornerRadius::same(rounding.clamp(0.0, 255.0) as u8);
737 painter.rect_stroke(
738 rect,
739 corner,
740 egui::Stroke::new(thickness, color_to_egui(color)),
741 egui::epaint::StrokeKind::Inside,
742 );
743 }
744
745 fn overlay_text(
746 &mut self,
747 pos: [f32; 2],
748 text: &str,
749 size: f32,
750 color: [f32; 4],
751 family: FontFamilyHint,
752 ) {
753 let painter = self.overlay_painter();
754 let font_id = font_id_for(family, size);
755 painter.text(
756 egui::pos2(pos[0], pos[1]),
757 egui::Align2::LEFT_TOP,
758 text,
759 font_id,
760 color_to_egui(color),
761 );
762 }
763
764 fn tooltip_for_last(&mut self, text: &str) {
765 if let Some(response) = self.last_response.as_ref() {
766 response.clone().on_hover_text(text);
767 }
768 }
769
770 fn region_at(&mut self, id_salt: &str, rect: [f32; 4], f: &mut dyn FnMut(&mut dyn UiBuilder)) {
771 let r =
772 egui::Rect::from_min_size(egui::pos2(rect[0], rect[1]), egui::vec2(rect[2], rect[3]));
773 let vt = self.viewport_textures;
774 let id_salt = ("khora_region", id_salt);
780 let mut child = self.ui.new_child(
781 egui::UiBuilder::new()
782 .max_rect(r)
783 .id_salt(id_salt)
784 .layout(egui::Layout::top_down(egui::Align::Min)),
785 );
786 let mut nested = EguiUiBuilder::new(&mut child, vt);
787 f(&mut nested);
788 }
789
790 fn cursor_pos(&self) -> [f32; 2] {
791 let p = self.ui.next_widget_position();
792 [p.x, p.y]
793 }
794
795 fn allocate_size(&mut self, size: [f32; 2]) -> [f32; 4] {
796 let (rect, _) = self
797 .ui
798 .allocate_exact_size(egui::vec2(size[0], size[1]), egui::Sense::hover());
799 [rect.min.x, rect.min.y, rect.width(), rect.height()]
800 }
801
802 fn measure_text(&self, text: &str, size: f32, family: FontFamilyHint) -> [f32; 2] {
803 let font_id = font_id_for(family, size);
804 let galley =
807 self.ui
808 .painter()
809 .layout_no_wrap(text.to_owned(), font_id, egui::Color32::WHITE);
810 let r = galley.rect;
811 [r.width(), r.height()]
812 }
813
814 fn top_inset_panel(&mut self, id: &str, height: f32, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
817 let vt = self.viewport_textures;
818 egui::Panel::top(egui::Id::new(id.to_owned()))
819 .exact_size(height)
820 .resizable(false)
821 .frame(egui::Frame::new())
822 .show_inside(self.ui, |ui| {
823 let mut nested = EguiUiBuilder::new(ui, vt);
824 f(&mut nested);
825 });
826 }
827
828 fn bottom_inset_panel(&mut self, id: &str, height: f32, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
829 let vt = self.viewport_textures;
830 egui::Panel::bottom(egui::Id::new(id.to_owned()))
831 .exact_size(height)
832 .resizable(false)
833 .frame(egui::Frame::new())
834 .show_inside(self.ui, |ui| {
835 let mut nested = EguiUiBuilder::new(ui, vt);
836 f(&mut nested);
837 });
838 }
839
840 fn left_inset_panel(&mut self, id: &str, width: f32, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
841 let vt = self.viewport_textures;
842 egui::Panel::left(egui::Id::new(id.to_owned()))
843 .exact_size(width)
844 .resizable(false)
845 .frame(egui::Frame::new())
846 .show_inside(self.ui, |ui| {
847 let mut nested = EguiUiBuilder::new(ui, vt);
848 f(&mut nested);
849 });
850 }
851
852 fn right_inset_panel(&mut self, id: &str, width: f32, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
853 let vt = self.viewport_textures;
854 egui::Panel::right(egui::Id::new(id.to_owned()))
855 .exact_size(width)
856 .resizable(false)
857 .frame(egui::Frame::new())
858 .show_inside(self.ui, |ui| {
859 let mut nested = EguiUiBuilder::new(ui, vt);
860 f(&mut nested);
861 });
862 }
863
864 fn central_inset(&mut self, f: &mut dyn FnMut(&mut dyn UiBuilder)) {
865 let vt = self.viewport_textures;
866 egui::CentralPanel::default()
867 .frame(egui::Frame::new())
868 .show_inside(self.ui, |ui| {
869 let mut nested = EguiUiBuilder::new(ui, vt);
870 f(&mut nested);
871 });
872 }
873
874 fn modal(&mut self, id: &str, size: [f32; 2], f: &mut dyn FnMut(&mut dyn UiBuilder)) {
875 self.show_modal_dialog(id, size, f);
876 }
877
878 fn frame_box(
879 &mut self,
880 margin: khora_core::ui::Margin,
881 fill: Option<khora_core::math::LinearRgba>,
882 stroke: khora_core::ui::Stroke,
883 radius: khora_core::ui::CornerRadius,
884 f: &mut dyn FnMut(&mut dyn UiBuilder),
885 ) {
886 let mut frame = egui::Frame::new()
887 .inner_margin(egui::Margin {
888 left: margin.left as i8,
889 right: margin.right as i8,
890 top: margin.top as i8,
891 bottom: margin.bottom as i8,
892 })
893 .corner_radius(egui::CornerRadius {
894 nw: radius.nw as u8,
895 ne: radius.ne as u8,
896 sw: radius.sw as u8,
897 se: radius.se as u8,
898 });
899 if let Some(fill_color) = fill {
900 frame = frame.fill(linear_to_color(fill_color));
901 }
902 if stroke.width > 0.0 {
903 frame = frame.stroke(egui::Stroke::new(
904 stroke.width,
905 linear_to_color(stroke.color),
906 ));
907 }
908 let vt = self.viewport_textures;
909 frame.show(self.ui, |ui| {
910 let mut nested = EguiUiBuilder::new(ui, vt);
911 f(&mut nested);
912 });
913 }
914}
915
916fn linear_to_color(c: khora_core::math::LinearRgba) -> egui::Color32 {
917 color_to_egui([c.r, c.g, c.b, c.a])
918}
919
920impl EguiUiBuilder<'_> {
921 fn show_modal_dialog(
925 &mut self,
926 id: &str,
927 size: [f32; 2],
928 f: &mut dyn FnMut(&mut dyn UiBuilder),
929 ) {
930 let ctx = self.ui.ctx().clone();
931 let vt_clone = self.viewport_textures.clone();
932
933 let screen = ctx.input(|i| i.viewport().inner_rect.unwrap_or(egui::Rect::ZERO));
938 let layer = egui::LayerId::new(
939 egui::Order::Middle,
940 egui::Id::new(format!("{}_backdrop", id)),
941 );
942 let painter = egui::Painter::new(ctx.clone(), layer, screen);
943 painter.rect_filled(screen, 0.0, egui::Color32::from_black_alpha(140));
944
945 let window_id = egui::Id::new(format!("{}_modal", id));
946 egui::Window::new("")
947 .id(window_id)
948 .title_bar(false)
949 .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
950 .fixed_size([size[0], size[1]])
951 .resizable(false)
952 .collapsible(false)
953 .frame(egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::same(0)))
954 .show(&ctx, |ui| {
955 let mut nested = EguiUiBuilder::new(ui, &vt_clone);
956 f(&mut nested);
957 });
958 }
959}