1use crate::Vec2;
2use crate::color::{Brush, Color};
3use crate::geometry::Rect;
4use crate::input::{Modifiers, PointerKind};
5use crate::runtime::{Frame, HitRegion};
6use crate::shortcuts::DragAction;
7use crate::text::{FontStyle, FontWeight, TextAlign, TextDecoration};
8use crate::view::{Scene, SceneNode};
9use std::cell::RefCell;
10use std::{any::Any, path::PathBuf, rc::Rc, sync::Arc};
11use web_time::Instant;
12
13pub type DragPayload = Rc<dyn Any>;
16
17pub fn drag_payload<T: 'static>(value: T) -> DragPayload {
23 Rc::new(value)
24}
25
26pub fn downcast_drag_payload<T: 'static>(payload: &DragPayload) -> Option<&T> {
34 payload.as_ref().downcast_ref::<T>()
35}
36
37pub fn drag_and_drop_source<T, F>(mut modifier: crate::Modifier, on_start: F) -> crate::Modifier
51where
52 T: 'static,
53 F: Fn(DragStart) -> Option<T> + 'static,
54{
55 modifier = modifier.on_drag_start(move |start| on_start(start).map(drag_payload::<T>));
56 modifier
57}
58
59pub fn drag_and_drop_source_with_preview<T, F>(
61 mut modifier: crate::Modifier,
62 preview: DragPreview,
63 on_start: F,
64) -> crate::Modifier
65where
66 T: 'static,
67 F: Fn(DragStart) -> Option<T> + 'static,
68{
69 modifier = modifier
70 .draw_drag_decoration_rc(preview)
71 .on_drag_start(move |start| on_start(start).map(drag_payload::<T>));
72 modifier
73}
74
75pub fn drag_and_drop_target<T, F>(mut modifier: crate::Modifier, on_drop: F) -> crate::Modifier
88where
89 T: 'static,
90 F: Fn(&DropEvent, &T) -> bool + 'static,
91{
92 modifier = modifier.on_drop(move |ev| match downcast_drag_payload::<T>(&ev.payload) {
93 Some(v) => on_drop(&ev, v),
94 None => false,
95 });
96 modifier
97}
98
99pub trait DragDropModifierExt: Sized {
101 fn drag_source<T>(
103 self,
104 make_payload: impl Fn(DragStart) -> Option<T> + 'static,
105 ) -> crate::Modifier
106 where
107 T: 'static;
108
109 fn on_drag_enter_typed<T>(
111 self,
112 f: impl Fn(&DragOver, &T) + 'static,
113 ) -> crate::Modifier
114 where
115 T: 'static;
116
117 fn on_drag_over_typed<T>(
119 self,
120 f: impl Fn(&DragOver, &T) + 'static,
121 ) -> crate::Modifier
122 where
123 T: 'static;
124
125 fn on_drag_leave_typed<T>(
127 self,
128 f: impl Fn(&DragOver, &T) + 'static,
129 ) -> crate::Modifier
130 where
131 T: 'static;
132
133 fn on_drop_typed<T>(
135 self,
136 f: impl Fn(&DropEvent, &T) -> bool + 'static,
137 ) -> crate::Modifier
138 where
139 T: 'static;
140
141 fn drop_target<T>(
143 self,
144 on_drop: impl Fn(&DropEvent, &T) -> bool + 'static,
145 ) -> crate::Modifier
146 where
147 T: 'static;
148}
149
150impl DragDropModifierExt for crate::Modifier {
151 fn drag_source<T>(
152 self,
153 make_payload: impl Fn(DragStart) -> Option<T> + 'static,
154 ) -> crate::Modifier
155 where
156 T: 'static,
157 {
158 self.on_drag_start(move |start| make_payload(start).map(drag_payload::<T>))
159 }
160
161 fn on_drag_enter_typed<T>(
162 self,
163 f: impl Fn(&DragOver, &T) + 'static,
164 ) -> crate::Modifier
165 where
166 T: 'static,
167 {
168 self.on_drag_enter(move |ev| {
169 if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
170 f(&ev, payload);
171 }
172 })
173 }
174
175 fn on_drag_over_typed<T>(
176 self,
177 f: impl Fn(&DragOver, &T) + 'static,
178 ) -> crate::Modifier
179 where
180 T: 'static,
181 {
182 self.on_drag_over(move |ev| {
183 if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
184 f(&ev, payload);
185 }
186 })
187 }
188
189 fn on_drag_leave_typed<T>(
190 self,
191 f: impl Fn(&DragOver, &T) + 'static,
192 ) -> crate::Modifier
193 where
194 T: 'static,
195 {
196 self.on_drag_leave(move |ev| {
197 if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
198 f(&ev, payload);
199 }
200 })
201 }
202
203 fn on_drop_typed<T>(
204 self,
205 f: impl Fn(&DropEvent, &T) -> bool + 'static,
206 ) -> crate::Modifier
207 where
208 T: 'static,
209 {
210 self.on_drop(move |ev| {
211 let Some(payload) = downcast_drag_payload::<T>(&ev.payload) else {
212 return false;
213 };
214 f(&ev, payload)
215 })
216 }
217
218 fn drop_target<T>(
219 self,
220 on_drop: impl Fn(&DropEvent, &T) -> bool + 'static,
221 ) -> crate::Modifier
222 where
223 T: 'static,
224 {
225 self.on_drop_typed(on_drop)
226 }
227}
228
229#[derive(Clone, Debug)]
230pub struct DragStart {
231 pub source_id: u64,
232 pub position: Vec2,
233 pub modifiers: Modifiers,
234}
235
236#[derive(Clone, Debug)]
237pub struct DragOver {
238 pub source_id: u64,
239 pub target_id: u64,
240 pub position: Vec2,
241 pub modifiers: Modifiers,
242 pub payload: DragPayload,
243}
244
245#[derive(Clone, Debug)]
246pub struct DropEvent {
247 pub source_id: u64,
248 pub target_id: u64,
249 pub position: Vec2,
250 pub modifiers: Modifiers,
251 pub payload: DragPayload,
252}
253
254#[derive(Clone, Copy, Debug)]
256pub struct DragEnd {
257 pub accepted: bool,
258}
259
260#[derive(Clone)]
262pub struct DragPreviewCtx {
263 pub pointer: Vec2,
265 pub start_pointer: Vec2,
267 pub source_rect: Rect,
269 pub grab_offset: Vec2,
271 pub payload: DragPayload,
273}
274
275pub type DragPreview = Rc<dyn Fn(&mut Scene, &DragPreviewCtx)>;
278
279thread_local! {
281 static PENDING_PREVIEW: RefCell<Option<DragPreview>> = const { RefCell::new(None) };
282}
283
284pub fn provide_drag_preview(preview: DragPreview) {
293 PENDING_PREVIEW.with(|p| *p.borrow_mut() = Some(preview));
294}
295
296fn take_pending_preview() -> Option<DragPreview> {
297 PENDING_PREVIEW.with(|p| p.borrow_mut().take())
298}
299
300pub fn drag_preview_label(label: impl Into<String>, accent: Color) -> DragPreview {
302 let label = label.into();
303 Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
304 draw_label_chip(scene, ctx.pointer, &label, accent, false);
305 })
306}
307
308pub fn drag_preview_chip(label: impl Into<String>, accent: Color) -> DragPreview {
310 let label = label.into();
311 Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
312 draw_label_chip(scene, ctx.pointer, &label, accent, true);
313 })
314}
315
316fn draw_label_chip(
317 scene: &mut Scene,
318 pointer: Vec2,
319 label: &str,
320 accent: Color,
321 elevated: bool,
322) {
323 let ts = crate::locals::text_scale().0;
324 let pad_x = crate::locals::dp_to_px(10.0);
325 let pad_y = crate::locals::dp_to_px(6.0);
326 let font_px = crate::locals::dp_to_px(13.0) * ts;
327 let text_w = (label.chars().count() as f32 * font_px * 0.55)
329 .max(crate::locals::dp_to_px(24.0));
330 let w = text_w + pad_x * 2.0;
331 let h = font_px + pad_y * 2.0;
332 let r = crate::locals::dp_to_px(8.0);
333
334 let origin = Vec2 {
335 x: pointer.x + crate::locals::dp_to_px(14.0),
336 y: pointer.y + crate::locals::dp_to_px(14.0),
337 };
338 let rect = Rect {
339 x: origin.x,
340 y: origin.y,
341 w,
342 h,
343 };
344
345 if elevated {
346 scene.nodes.push(SceneNode::Rect {
348 rect: Rect {
349 x: rect.x + crate::locals::dp_to_px(2.0),
350 y: rect.y + crate::locals::dp_to_px(3.0),
351 w: rect.w,
352 h: rect.h,
353 },
354 brush: Brush::Solid(Color::from_rgba(0, 0, 0, 50)),
355 radius: [r; 4],
356 });
357 }
358
359 let bg = accent.with_alpha(0xDD);
360 scene.nodes.push(SceneNode::Rect {
361 rect,
362 brush: Brush::Solid(bg),
363 radius: [r; 4],
364 });
365 scene.nodes.push(SceneNode::Border {
366 rect,
367 color: accent.with_alpha(0xFF),
368 width: crate::locals::dp_to_px(1.0),
369 radius: [r; 4],
370 });
371 scene.nodes.push(SceneNode::Text {
372 rect: Rect {
373 x: rect.x + pad_x,
374 y: rect.y + pad_y,
375 w: text_w,
376 h: font_px,
377 },
378 text: Arc::<str>::from(label),
379 color: Color::WHITE,
380 size: font_px,
381 font_family: None,
382 text_align: TextAlign::Unspecified,
383 font_weight: FontWeight::MEDIUM,
384 font_style: FontStyle::Normal,
385 text_decoration: TextDecoration::default(),
386 letter_spacing: 0.0,
387 line_height: 0.0,
388 extra_style: Default::default(),
389 url: None,
390 font_variation_settings: None,
391 });
392}
393
394fn draw_default_source_ghost(scene: &mut Scene, ctx: &DragPreviewCtx, accent: Color) {
396 let w = ctx.source_rect.w.max(crate::locals::dp_to_px(24.0));
397 let h = ctx.source_rect.h.max(crate::locals::dp_to_px(16.0));
398 let rect = Rect {
399 x: ctx.pointer.x - ctx.grab_offset.x,
400 y: ctx.pointer.y - ctx.grab_offset.y,
401 w,
402 h,
403 };
404 let r = crate::locals::dp_to_px(6.0);
405 scene.nodes.push(SceneNode::Rect {
406 rect,
407 brush: Brush::Solid(accent.with_alpha(0x55)),
408 radius: [r; 4],
409 });
410 scene.nodes.push(SceneNode::Border {
411 rect,
412 color: accent.with_alpha(0xCC),
413 width: crate::locals::dp_to_px(1.5),
414 radius: [r; 4],
415 });
416}
417
418#[derive(Clone, Debug)]
422pub struct DroppedFile {
423 pub name: String,
424 pub path: Option<PathBuf>,
425}
426
427#[derive(Clone, Debug)]
429pub struct DroppedFiles {
430 pub files: Vec<DroppedFile>,
431}
432
433#[derive(Clone)]
435pub struct DragSession {
436 pub source_id: u64,
437 pub payload: DragPayload,
438 pub start_px: (f32, f32),
439 pub over_id: Option<u64>,
440 pub source_rect: Rect,
442 pub grab_offset: Vec2,
444 pub preview: Option<DragPreview>,
446}
447
448impl std::fmt::Debug for DragSession {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 f.debug_struct("DragSession")
452 .field("source_id", &self.source_id)
453 .field("start_px", &self.start_px)
454 .field("over_id", &self.over_id)
455 .field("source_rect", &self.source_rect)
456 .field("grab_offset", &self.grab_offset)
457 .field("preview", &self.preview.as_ref().map(|_| "…"))
458 .finish_non_exhaustive()
459 }
460}
461
462#[derive(Clone)]
463struct MouseDownState {
464 position: Vec2,
465 capture_id: u64,
466}
467
468#[derive(Clone)]
469struct TouchDownState {
470 time: Instant,
471 position: Vec2,
472 capture_id: u64,
473 long_press_pending: bool,
474}
475
476const LONG_PRESS_MS: u128 = 400;
477
478thread_local! {
479 static DND_FRAME: RefCell<Option<Frame>> = const { RefCell::new(None) };
480 static DND_SCALE: RefCell<f32> = const { RefCell::new(1.0) };
481 static DND_SESSION: RefCell<Option<DragSession>> = const { RefCell::new(None) };
482 static DND_MOUSE_DOWN: RefCell<Option<MouseDownState>> = const { RefCell::new(None) };
483 static DND_TOUCH_DOWN: RefCell<Option<TouchDownState>> = const { RefCell::new(None) };
484}
485
486pub fn set_dnd_frame(frame: Option<Frame>) {
488 DND_FRAME.with(|f| *f.borrow_mut() = frame);
489}
490
491pub fn set_dnd_scale(scale: f32) {
493 DND_SCALE.with(|s| *s.borrow_mut() = scale);
494}
495
496pub fn is_dragging() -> bool {
498 DND_SESSION.with(|s| s.borrow().is_some())
499}
500
501pub fn current_drag_session() -> Option<DragSession> {
503 DND_SESSION.with(|s| s.borrow().clone())
504}
505
506fn touch_slop_px(scale: f32) -> f32 {
507 6.0 * scale
508}
509
510fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
511 frame.hit_regions.iter().position(|h| h.id == id)
512}
513
514fn is_dnd_target(hit: &HitRegion) -> bool {
515 hit.on_drop.is_some()
516 || hit.on_drag_enter.is_some()
517 || hit.on_drag_over.is_some()
518 || hit.on_drag_leave.is_some()
519}
520
521pub fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
522 frame
523 .hit_regions
524 .iter()
525 .rev()
526 .filter(|h| h.rect.contains(pos))
527 .find(|h| is_dnd_target(h))
528 .map(|h| h.id)
529}
530
531fn dnd_update_over(frame: &Frame, session: &mut DragSession, modifiers: Modifiers, pos: Vec2) {
532 let new_over = dnd_target_id_at(frame, pos);
533
534 if new_over != session.over_id {
535 if let Some(prev) = session.over_id {
536 if let Some(i) = hit_index_by_id(frame, prev) {
537 if let Some(cb) = &frame.hit_regions[i].on_drag_leave {
538 cb(DragOver {
539 source_id: session.source_id,
540 target_id: prev,
541 position: pos,
542 modifiers,
543 payload: session.payload.clone(),
544 });
545 }
546 }
547 }
548
549 if let Some(now) = new_over {
550 if let Some(i) = hit_index_by_id(frame, now) {
551 if let Some(cb) = &frame.hit_regions[i].on_drag_enter {
552 cb(DragOver {
553 source_id: session.source_id,
554 target_id: now,
555 position: pos,
556 modifiers,
557 payload: session.payload.clone(),
558 });
559 }
560 }
561 }
562
563 session.over_id = new_over;
564 }
565
566 if let Some(over) = session.over_id {
567 if let Some(i) = hit_index_by_id(frame, over) {
568 if let Some(cb) = &frame.hit_regions[i].on_drag_over {
569 cb(DragOver {
570 source_id: session.source_id,
571 target_id: over,
572 position: pos,
573 modifiers,
574 payload: session.payload.clone(),
575 });
576 }
577 }
578 }
579}
580
581fn dnd_finish(
583 frame: &Frame,
584 session: DragSession,
585 modifiers: Modifiers,
586 pos: Vec2,
587 accept_if_possible: bool,
588) -> bool {
589 let mut accepted = false;
590 if accept_if_possible {
591 let drop_target = dnd_target_id_at(frame, pos);
592 if let Some(tid) = drop_target {
593 if let Some(i) = hit_index_by_id(frame, tid) {
594 if let Some(cb) = &frame.hit_regions[i].on_drop {
595 accepted = cb(DropEvent {
596 source_id: session.source_id,
597 target_id: tid,
598 position: pos,
599 modifiers,
600 payload: session.payload.clone(),
601 });
602 }
603 }
604 }
605 }
606
607 if let Some(i) = hit_index_by_id(frame, session.source_id) {
608 if let Some(cb) = &frame.hit_regions[i].on_drag_end {
609 cb(DragEnd { accepted });
610 }
611 }
612
613 accepted
614}
615
616fn initiate_drag(
617 frame: &Frame,
618 capture_id: u64,
619 start_pos: Vec2,
620 current_pos: Vec2,
621 modifiers: Modifiers,
622) -> bool {
623 let Some(i) = hit_index_by_id(frame, capture_id) else {
624 return false;
625 };
626 let Some(cb) = &frame.hit_regions[i].on_drag_start else {
627 return false;
628 };
629
630 let _ = take_pending_preview();
632
633 let payload = cb(DragStart {
634 source_id: capture_id,
635 position: current_pos,
636 modifiers,
637 });
638 let Some(payload) = payload else {
639 let _ = take_pending_preview();
640 return false;
641 };
642
643 let preview = take_pending_preview().or_else(|| frame.hit_regions[i].drag_preview.clone());
645
646 let source_rect = frame.hit_regions[i].rect;
647 let grab_offset = Vec2 {
648 x: start_pos.x - source_rect.x,
649 y: start_pos.y - source_rect.y,
650 };
651
652 DND_SESSION.with(|s| {
653 *s.borrow_mut() = Some(DragSession {
654 source_id: capture_id,
655 payload,
656 start_px: (start_pos.x, start_pos.y),
657 over_id: None,
658 source_rect,
659 grab_offset,
660 preview,
661 });
662 });
663 true
664}
665
666pub fn handle_drag_action(action: &DragAction) -> bool {
668 let scale = DND_SCALE.with(|s| *s.borrow());
669 let slop = touch_slop_px(scale);
670
671 match *action {
672 DragAction::Press {
673 position,
674 capture_id,
675 kind,
676 ..
677 } => {
678 match kind {
679 PointerKind::Mouse => {
680 DND_MOUSE_DOWN.with(|m| {
681 *m.borrow_mut() = Some(MouseDownState {
682 position,
683 capture_id,
684 });
685 });
686 }
687 _ => {
688 DND_TOUCH_DOWN.with(|t| {
690 *t.borrow_mut() = Some(TouchDownState {
691 time: web_time::Instant::now(),
692 position,
693 capture_id,
694 long_press_pending: true,
695 });
696 });
697 }
698 }
699 false
700 }
701
702 DragAction::Move {
703 position,
704 modifiers,
705 } => {
706 if DND_SESSION.with(|s| s.borrow().is_some()) {
708 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
709 DND_SESSION.with(|s| {
710 if let Some(ref mut session) = *s.borrow_mut() {
711 dnd_update_over(&frame, session, modifiers, position);
712 }
713 });
714 }
715 return true;
716 }
717
718 if let Some(down) = DND_MOUSE_DOWN.with(|m| m.borrow().clone()) {
720 let dx = position.x - down.position.x;
721 let dy = position.y - down.position.y;
722 let dist = (dx * dx + dy * dy).sqrt();
723 if dist >= slop {
724 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
725 if initiate_drag(
726 &frame,
727 down.capture_id,
728 down.position,
729 position,
730 modifiers,
731 ) {
732 DND_SESSION.with(|s| {
734 if let Some(ref mut session) = *s.borrow_mut() {
735 dnd_update_over(&frame, session, modifiers, position);
736 }
737 });
738 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
739 return true;
740 }
741 }
742 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
745 }
746 return true; }
748
749 if let Some(touch) = DND_TOUCH_DOWN.with(|t| t.borrow().clone()) {
751 if touch.long_press_pending {
752 let elapsed_ms = (Instant::now() - touch.time).as_millis() as u128;
753 let dx = position.x - touch.position.x;
754 let dy = position.y - touch.position.y;
755 let dist = (dx * dx + dy * dy).sqrt();
756
757 if elapsed_ms >= LONG_PRESS_MS && dist <= slop {
758 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
759 if initiate_drag(
760 &frame,
761 touch.capture_id,
762 touch.position,
763 position,
764 modifiers,
765 ) {
766 DND_SESSION.with(|s| {
767 if let Some(ref mut session) = *s.borrow_mut() {
768 dnd_update_over(&frame, session, modifiers, position);
769 }
770 });
771 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
772 return true;
773 }
774 DND_TOUCH_DOWN.with(|t| {
776 if let Some(ref mut td) = *t.borrow_mut() {
777 td.long_press_pending = false;
778 }
779 });
780 }
781 }
782 if dist > slop {
783 DND_TOUCH_DOWN.with(|t| {
784 if let Some(ref mut td) = *t.borrow_mut() {
785 td.long_press_pending = false;
786 }
787 });
788 }
789 }
790 let still_pending = DND_TOUCH_DOWN.with(|t| {
793 t.borrow().as_ref().map(|td| td.long_press_pending).unwrap_or(false)
794 });
795 if still_pending {
796 return true;
797 }
798 }
799
800 false
801 }
802
803 DragAction::Release {
804 position,
805 modifiers,
806 } => {
807 let mut consumed = false;
808
809 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
810 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
811 dnd_finish(&frame, session, modifiers, position, true);
812 }
813 consumed = true;
814 }
815
816 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
817 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
818
819 consumed
820 }
821
822 DragAction::Cancel => {
823 let mut consumed = false;
824 if let Some(session) = DND_SESSION.with(|s| s.borrow_mut().take()) {
825 if let Some(frame) = DND_FRAME.with(|f| f.borrow().clone()) {
826 dnd_finish(
827 &frame,
828 session,
829 Modifiers::default(),
830 Vec2::default(),
831 false,
832 );
833 }
834 consumed = true;
835 }
836 DND_MOUSE_DOWN.with(|m| *m.borrow_mut() = None);
837 DND_TOUCH_DOWN.with(|t| *t.borrow_mut() = None);
838 consumed
839 }
840 }
841}
842
843pub fn overlay_drag_indicator(
846 scene: &mut Scene,
847 mouse_pos_px: (f32, f32),
848 external_file_drag: bool,
849) {
850 if !is_dragging() && !external_file_drag {
851 return;
852 }
853
854 let pos = Vec2 {
855 x: mouse_pos_px.0,
856 y: mouse_pos_px.1,
857 };
858
859 let frame = DND_FRAME.with(|f| f.borrow().clone());
860 let Some(ref f) = frame else {
861 return;
862 };
863
864 let accent = if external_file_drag {
865 Color::from_hex("#FFAA00")
866 } else {
867 Color::from_hex("#44AAFF")
868 };
869
870 if let Some(tid) = dnd_target_id_at(f, pos)
872 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
873 {
874 let r = crate::locals::dp_to_px(8.0);
875 scene.nodes.push(SceneNode::Border {
876 rect: hit.rect,
877 color: accent,
878 width: crate::locals::dp_to_px(2.0),
879 radius: [r; 4],
880 });
881 }
882
883 if external_file_drag {
884 draw_label_chip(scene, pos, "Drop files", accent, true);
885 return;
886 }
887
888 DND_SESSION.with(|s| {
889 let session = s.borrow();
890 let Some(ref session) = *session else {
891 return;
892 };
893
894 let ctx = DragPreviewCtx {
895 pointer: pos,
896 start_pointer: Vec2 {
897 x: session.start_px.0,
898 y: session.start_px.1,
899 },
900 source_rect: session.source_rect,
901 grab_offset: session.grab_offset,
902 payload: session.payload.clone(),
903 };
904
905 if let Some(ref preview) = session.preview {
906 preview(scene, &ctx);
907 } else {
908 draw_default_source_ghost(scene, &ctx, accent);
909 }
910 });
911}