1use crate::*;
2use repose_core::HitRegion;
3use repose_core::Modifiers;
4use repose_core::Vec2;
5use repose_core::dnd::{DragOver, DragPayload, DropEvent};
6use repose_core::input::{PointerButton, PointerEvent, PointerEventKind, PointerId, PointerKind};
7use repose_core::locals::dp_to_px;
8use repose_core::runtime::Frame;
9use repose_ui::TextFieldState;
10use repose_ui::textfield::{
11 TF_FONT_DP, TF_PADDING_X_DP, caret_xy_for_byte, index_for_x_bytes, index_for_xy_bytes,
12 measure_text,
13};
14
15pub(crate) fn tick_snackbar(last_redraw: web_time::Instant) {
16 let now = web_time::Instant::now();
17 let elapsed = now.saturating_duration_since(last_redraw);
18 let ms = elapsed.as_millis().min(u32::MAX as u128) as u32;
19 if ms > 0 {
20 repose_ui::overlay::SnackbarController::tick_for_frame(ms);
21 }
22}
23
24pub(crate) fn request_redraw(window: &Option<std::sync::Arc<winit::window::Window>>) {
25 if let Some(w) = window {
26 w.request_redraw();
27 }
28}
29
30pub(crate) fn tf_key_of_in_frame(frame_cache: &Option<Frame>, visual_id: u64) -> u64 {
31 if let Some(f) = frame_cache {
32 return tf_key_of(f, visual_id);
33 }
34 visual_id
35}
36
37pub(crate) fn is_textfield_in_frame(frame_cache: &Option<Frame>, id: u64) -> bool {
38 if let Some(f) = frame_cache {
39 f.semantics_nodes
40 .iter()
41 .any(|n| n.id == id && n.role == Role::TextField)
42 } else {
43 false
44 }
45}
46
47pub(crate) fn dnd_update_over_in_frame(
48 frame_cache: &Option<Frame>,
49 drag: &mut Option<DragSession>,
50 modifiers: Modifiers,
51 pos: Vec2,
52) {
53 let Some(f) = frame_cache else {
54 return;
55 };
56 let Some(session) = drag.as_mut() else {
57 return;
58 };
59 dnd_update_over(f, session, modifiers, pos);
60}
61
62pub(crate) fn update_modifiers(modifiers: &mut Modifiers, state: &winit::keyboard::ModifiersState) {
63 modifiers.shift = state.shift_key();
64 modifiers.ctrl = state.control_key();
65 modifiers.alt = state.alt_key();
66 modifiers.meta = state.super_key();
67 modifiers.command = if cfg!(target_os = "macos") {
68 modifiers.meta
69 } else {
70 modifiers.ctrl
71 };
72}
73
74pub(crate) fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
77 if let Some(vt) = &state.visual_transformation {
78 let tfmd = vt.filter(&state.text);
79 let display_idx = index_for_x_bytes(&tfmd.text, font_px, x_px);
80 (tfmd.offset_map)(display_idx)
81 } else {
82 index_for_x_bytes(&state.text, font_px, x_px)
83 }
84}
85
86pub(crate) fn index_for_xy_bytes_vt(
88 state: &TextFieldState,
89 font_px: f32,
90 wrap_w: f32,
91 x_px: f32,
92 y_px: f32,
93) -> usize {
94 if let Some(vt) = &state.visual_transformation {
95 let tfmd = vt.filter(&state.text);
96 let display_idx = index_for_xy_bytes(&tfmd.text, font_px, wrap_w, x_px, y_px);
97 (tfmd.offset_map)(display_idx)
98 } else {
99 index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
100 }
101}
102
103pub(crate) fn top_hit_index(frame: &Frame, pos: Vec2) -> Option<usize> {
105 frame
106 .hit_regions
107 .iter()
108 .enumerate()
109 .rev()
110 .find(|(_, h)| h.rect.contains(pos))
111 .map(|(i, _)| i)
112}
113
114pub(crate) fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
115 frame.hit_regions.iter().position(|h| h.id == id)
116}
117
118pub(crate) fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
119 if let Some(i) = hit_index_by_id(frame, visual_id) {
120 let hr = &frame.hit_regions[i];
121 return hr.tf_state_key.unwrap_or(hr.id);
122 }
123 visual_id
124}
125
126pub(crate) fn pe_mouse(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
127 PointerEvent {
128 id: PointerId(0),
129 kind: PointerKind::Mouse,
130 event,
131 position: pos,
132 pressure: 1.0,
133 modifiers: mods,
134 }
135}
136
137pub(crate) fn pe_touch(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
138 PointerEvent {
139 id: PointerId(0),
140 kind: PointerKind::Touch,
141 event,
142 position: pos,
143 pressure: 1.0,
144 modifiers: mods,
145 }
146}
147
148pub(crate) fn pe_down_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
149 PointerEvent {
150 id: PointerId(0),
151 kind,
152 event: PointerEventKind::Down(PointerButton::Primary),
153 position: pos,
154 pressure: 1.0,
155 modifiers: mods,
156 }
157}
158
159pub(crate) fn pe_up_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
160 PointerEvent {
161 id: PointerId(0),
162 kind,
163 event: PointerEventKind::Up(PointerButton::Primary),
164 position: pos,
165 pressure: 1.0,
166 modifiers: mods,
167 }
168}
169
170pub(crate) fn map_key(key: winit::keyboard::PhysicalKey) -> repose_core::input::Key {
171 use repose_core::input::Key;
172 use winit::keyboard::{KeyCode, PhysicalKey};
173
174 match key {
175 PhysicalKey::Code(KeyCode::Enter) => Key::Enter,
176 PhysicalKey::Code(KeyCode::Tab) => Key::Tab,
177 PhysicalKey::Code(KeyCode::Backspace) => Key::Backspace,
178 PhysicalKey::Code(KeyCode::Delete) => Key::Delete,
179 PhysicalKey::Code(KeyCode::Escape) => Key::Escape,
180 PhysicalKey::Code(KeyCode::ArrowLeft) => Key::ArrowLeft,
181 PhysicalKey::Code(KeyCode::ArrowRight) => Key::ArrowRight,
182 PhysicalKey::Code(KeyCode::ArrowUp) => Key::ArrowUp,
183 PhysicalKey::Code(KeyCode::ArrowDown) => Key::ArrowDown,
184 PhysicalKey::Code(KeyCode::Home) => Key::Home,
185 PhysicalKey::Code(KeyCode::End) => Key::End,
186 PhysicalKey::Code(KeyCode::PageUp) => Key::PageUp,
187 PhysicalKey::Code(KeyCode::PageDown) => Key::PageDown,
188 PhysicalKey::Code(KeyCode::Space) => Key::Space,
189 PhysicalKey::Code(KeyCode::KeyA) => Key::Character('a'),
190 PhysicalKey::Code(KeyCode::KeyB) => Key::Character('b'),
191 PhysicalKey::Code(KeyCode::KeyC) => Key::Character('c'),
192 PhysicalKey::Code(KeyCode::KeyD) => Key::Character('d'),
193 PhysicalKey::Code(KeyCode::KeyE) => Key::Character('e'),
194 PhysicalKey::Code(KeyCode::KeyF) => Key::Character('f'),
195 PhysicalKey::Code(KeyCode::KeyG) => Key::Character('g'),
196 PhysicalKey::Code(KeyCode::KeyH) => Key::Character('h'),
197 PhysicalKey::Code(KeyCode::KeyI) => Key::Character('i'),
198 PhysicalKey::Code(KeyCode::KeyJ) => Key::Character('j'),
199 PhysicalKey::Code(KeyCode::KeyK) => Key::Character('k'),
200 PhysicalKey::Code(KeyCode::KeyL) => Key::Character('l'),
201 PhysicalKey::Code(KeyCode::KeyM) => Key::Character('m'),
202 PhysicalKey::Code(KeyCode::KeyN) => Key::Character('n'),
203 PhysicalKey::Code(KeyCode::KeyO) => Key::Character('o'),
204 PhysicalKey::Code(KeyCode::KeyP) => Key::Character('p'),
205 PhysicalKey::Code(KeyCode::KeyQ) => Key::Character('q'),
206 PhysicalKey::Code(KeyCode::KeyR) => Key::Character('r'),
207 PhysicalKey::Code(KeyCode::KeyS) => Key::Character('s'),
208 PhysicalKey::Code(KeyCode::KeyT) => Key::Character('t'),
209 PhysicalKey::Code(KeyCode::KeyU) => Key::Character('u'),
210 PhysicalKey::Code(KeyCode::KeyV) => Key::Character('v'),
211 PhysicalKey::Code(KeyCode::KeyW) => Key::Character('w'),
212 PhysicalKey::Code(KeyCode::KeyX) => Key::Character('x'),
213 PhysicalKey::Code(KeyCode::KeyY) => Key::Character('y'),
214 PhysicalKey::Code(KeyCode::KeyZ) => Key::Character('z'),
215 PhysicalKey::Code(KeyCode::Digit0) => Key::Character('0'),
216 PhysicalKey::Code(KeyCode::Digit1) => Key::Character('1'),
217 PhysicalKey::Code(KeyCode::Digit2) => Key::Character('2'),
218 PhysicalKey::Code(KeyCode::Digit3) => Key::Character('3'),
219 PhysicalKey::Code(KeyCode::Digit4) => Key::Character('4'),
220 PhysicalKey::Code(KeyCode::Digit5) => Key::Character('5'),
221 PhysicalKey::Code(KeyCode::Digit6) => Key::Character('6'),
222 PhysicalKey::Code(KeyCode::Digit7) => Key::Character('7'),
223 PhysicalKey::Code(KeyCode::Digit8) => Key::Character('8'),
224 PhysicalKey::Code(KeyCode::Digit9) => Key::Character('9'),
225 PhysicalKey::Code(KeyCode::F1) => Key::F(1),
226 PhysicalKey::Code(KeyCode::F2) => Key::F(2),
227 PhysicalKey::Code(KeyCode::F3) => Key::F(3),
228 PhysicalKey::Code(KeyCode::F4) => Key::F(4),
229 PhysicalKey::Code(KeyCode::F5) => Key::F(5),
230 PhysicalKey::Code(KeyCode::F6) => Key::F(6),
231 PhysicalKey::Code(KeyCode::F7) => Key::F(7),
232 PhysicalKey::Code(KeyCode::F8) => Key::F(8),
233 PhysicalKey::Code(KeyCode::F9) => Key::F(9),
234 PhysicalKey::Code(KeyCode::F10) => Key::F(10),
235 PhysicalKey::Code(KeyCode::F11) => Key::F(11),
236 PhysicalKey::Code(KeyCode::F12) => Key::F(12),
237 _ => Key::Unknown,
238 }
239}
240
241pub(crate) fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
242 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
243 let wrap_width = state.inner_width;
244
245 if is_multiline {
246 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
247 let iw = state.inner_width;
248 let ih = state.inner_height;
249 state.ensure_caret_visible_xy(cx, cy, iw, ih, dp_to_px(2.0));
250 } else {
251 let caret_idx = state.caret_index();
252 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
253 let tfmd = vt.filter(&state.text);
254 let off = repose_core::original_offset_to_display(&state.text, &tfmd.text, caret_idx);
255 (tfmd.text, off)
256 } else {
257 (state.text.clone(), caret_idx)
258 };
259 let m = measure_text(&display, font_px, None);
260 let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
261 state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
262 }
263}
264
265pub(crate) fn tf_place_caret_at_pointer(
271 state: &mut TextFieldState,
272 hit_rect: Rect,
273 is_multiline: bool,
274 pos_px: (f32, f32),
275 scale: f32,
276 shift: bool,
277) {
278 let padding_px = TF_PADDING_X_DP * scale;
279 let inner_x_px = hit_rect.x + padding_px;
280 let inner_y_px = hit_rect.y + 8.0 * scale;
281 let content_x_px = (pos_px.0 - inner_x_px + state.scroll_offset).max(0.0);
282 let content_y_px = (pos_px.1 - inner_y_px + state.scroll_offset_y).max(0.0);
283 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
284
285 let idx = if is_multiline {
286 index_for_xy_bytes_vt(
287 state,
288 font_px,
289 hit_rect.w - 2.0 * padding_px,
290 content_x_px,
291 content_y_px,
292 )
293 } else {
294 index_for_x_bytes_vt(state, font_px, content_x_px)
295 };
296 state.begin_drag(idx, shift);
297
298 let caret_idx = state.caret_index();
300 let wrap_w = hit_rect.w - 2.0 * padding_px;
301 if is_multiline {
302 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_w, caret_idx);
303 let iw = state.inner_width;
304 let ih = state.inner_height;
305 state.ensure_caret_visible_xy(cx, cy, iw, ih, 2.0 * scale);
306 } else {
307 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
308 let tfmd = vt.filter(&state.text);
309 let off = repose_core::original_offset_to_display(&state.text, &tfmd.text, caret_idx);
310 (tfmd.text, off)
311 } else {
312 (state.text.clone(), caret_idx)
313 };
314 let m = measure_text(&display, font_px, None);
315 let cx = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
316 state.ensure_caret_visible(cx, wrap_w, 2.0 * scale);
317 }
318}
319
320pub(crate) fn touch_slop_px(scale: f32) -> f32 {
321 6.0 * scale
322}
323
324pub(crate) fn focus_in_direction(
327 chain: &[u64],
328 hit_regions: &[HitRegion],
329 current: Option<u64>,
330 dir: FocusDirection,
331) -> Option<u64> {
332 repose_core::spatial_focus_next(chain, hit_regions, current, dir)
333}
334
335pub(crate) fn is_dnd_target(hit: &HitRegion) -> bool {
336 hit.on_drop.is_some()
337 || hit.on_drag_enter.is_some()
338 || hit.on_drag_over.is_some()
339 || hit.on_drag_leave.is_some()
340}
341
342pub(crate) fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
343 frame
344 .hit_regions
345 .iter()
346 .rev()
347 .filter(|h| h.rect.contains(pos))
348 .find(|h| is_dnd_target(h))
349 .map(|h| h.id)
350}
351pub(crate) fn dispatch_scroll(
357 frame: &Frame,
358 pos: Vec2,
359 mut delta: Vec2,
360 scroll_capture: Option<u64>,
361) -> (bool, Option<u64>) {
362 let mut new_capture = scroll_capture;
363
364 if let Some(cid) = scroll_capture {
366 if let Some(cb) = frame
367 .hit_regions
368 .iter()
369 .find(|h| h.id == cid)
370 .and_then(|h| h.on_scroll.as_ref())
371 {
372 let before = delta;
373 let leftover = cb(before);
374 let consumed_x = (before.x - leftover.x).abs() > 0.001;
375 let consumed_y = (before.y - leftover.y).abs() > 0.001;
376 if consumed_x || consumed_y {
377 delta = leftover;
378 if delta.x.abs() <= 0.001 && delta.y.abs() <= 0.001 {
379 return (true, Some(cid));
380 }
381 } else {
382 new_capture = None; }
384 }
385 }
386
387 let mut any_consumed = false;
389 for hit in frame
390 .hit_regions
391 .iter()
392 .rev()
393 .filter(|h| h.rect.contains(pos))
394 {
395 if let Some(cb) = &hit.on_scroll {
396 let before = delta;
397 let leftover = cb(before);
398 let consumed_x = (before.x - leftover.x).abs() > 0.001;
399 let consumed_y = (before.y - leftover.y).abs() > 0.001;
400 if consumed_x || consumed_y {
401 any_consumed = true;
402 if new_capture.is_none() {
403 new_capture = Some(hit.id);
404 }
405 }
406 delta = leftover;
407 if delta.x.abs() <= 0.001 && delta.y.abs() <= 0.001 {
408 break;
409 }
410 }
411 }
412 (any_consumed, new_capture)
413}
414
415#[macro_export]
416macro_rules! handle_text_undo_redo {
417 ($app:expr, $key_event:expr) => {{
418 let mut __handled = false;
419 if $key_event.state == ElementState::Pressed && !$key_event.repeat && $app.modifiers.command
420 {
421 match $key_event.physical_key {
422 PhysicalKey::Code(KeyCode::KeyZ) if $app.modifiers.shift => {
423 if let Some(fid) = $app.sched.focused {
424 let key = $app.tf_key_of(fid);
425 if let Some(state_rc) = $app.textfield_states.get(&key) {
426 let mut st = state_rc.borrow_mut();
427 if st.can_redo() {
428 st.redo();
429 $app.notify_text_change(fid, st.text.clone());
430 __handled = true;
431 }
432 }
433 }
434 }
435 PhysicalKey::Code(KeyCode::KeyZ) => {
436 if let Some(fid) = $app.sched.focused {
437 let key = $app.tf_key_of(fid);
438 if let Some(state_rc) = $app.textfield_states.get(&key) {
439 let mut st = state_rc.borrow_mut();
440 if st.can_undo() {
441 st.undo();
442 $app.notify_text_change(fid, st.text.clone());
443 __handled = true;
444 }
445 }
446 }
447 }
448 _ => {}
449 }
450 }
451 __handled
452 }};
453}
454
455#[macro_export]
459macro_rules! handle_arrow_key_spatial_nav {
460 ($app:expr, $key_event:expr, $f:ident, $next:ident, $on_focus:expr) => {
461 if $key_event.state == ElementState::Pressed && !$key_event.repeat {
462 let nav_dir = match $key_event.physical_key {
463 PhysicalKey::Code(KeyCode::ArrowLeft) => Some(FocusDirection::Left),
464 PhysicalKey::Code(KeyCode::ArrowRight) => Some(FocusDirection::Right),
465 PhysicalKey::Code(KeyCode::ArrowUp) => Some(FocusDirection::Up),
466 PhysicalKey::Code(KeyCode::ArrowDown) => Some(FocusDirection::Down),
467 _ => None,
468 };
469 if let Some(dir) = nav_dir
470 && let Some($f) = &$app.frame_cache
471 && !$f
472 .semantics_nodes
473 .iter()
474 .any(|n| $app.sched.focused == Some(n.id) && n.role == Role::TextField)
475 {
476 if let Some($next) = $crate::common::focus_in_direction(
477 &$f.focus_chain,
478 &$f.hit_regions,
479 $app.sched.focused,
480 dir,
481 ) {
482 $app.sched.focused = Some($next);
483 $on_focus;
484 $app.request_redraw();
485 }
486 return; }
488 }
489 };
490}
491
492pub(crate) fn process_render_commands(
493 backend: &mut repose_render_wgpu::WgpuBackend,
494 cmds: Vec<RenderCommand>,
495) {
496 for cmd in cmds {
497 match cmd {
498 RenderCommand::SetImageEncoded {
499 handle,
500 bytes,
501 srgb,
502 } => {
503 let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
504 }
505 RenderCommand::SetImageRgba8 {
506 handle,
507 w,
508 h,
509 rgba,
510 srgb,
511 } => {
512 let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
513 }
514 RenderCommand::SetImageNv12 {
515 handle,
516 w,
517 h,
518 y,
519 uv,
520 full_range,
521 } => {
522 let _ = backend.set_image_nv12(handle, w, h, &y, &uv, full_range);
523 }
524 RenderCommand::RemoveImage { handle } => {
525 backend.remove_image(handle);
526 }
527 }
528 }
529}
530
531pub(crate) fn dnd_update_over(
533 frame: &Frame,
534 session: &mut DragSession,
535 modifiers: Modifiers,
536 pos: Vec2,
537) {
538 let new_over = dnd_target_id_at(frame, pos);
539
540 if new_over != session.over_id {
541 if let Some(prev) = session.over_id {
542 if let Some(i) = hit_index_by_id(frame, prev) {
543 if let Some(cb) = &frame.hit_regions[i].on_drag_leave {
544 cb(DragOver {
545 source_id: session.source_id,
546 target_id: prev,
547 position: pos,
548 modifiers,
549 payload: session.payload.clone(),
550 });
551 }
552 }
553 }
554
555 if let Some(now) = new_over {
556 if let Some(i) = hit_index_by_id(frame, now) {
557 if let Some(cb) = &frame.hit_regions[i].on_drag_enter {
558 cb(DragOver {
559 source_id: session.source_id,
560 target_id: now,
561 position: pos,
562 modifiers,
563 payload: session.payload.clone(),
564 });
565 }
566 }
567 }
568
569 session.over_id = new_over;
570 }
571
572 if let Some(over) = session.over_id {
573 if let Some(i) = hit_index_by_id(frame, over) {
574 if let Some(cb) = &frame.hit_regions[i].on_drag_over {
575 cb(DragOver {
576 source_id: session.source_id,
577 target_id: over,
578 position: pos,
579 modifiers,
580 payload: session.payload.clone(),
581 });
582 }
583 }
584 }
585}
586
587pub(crate) fn dnd_finish(
590 frame: &Frame,
591 session: DragSession,
592 modifiers: Modifiers,
593 pos: Vec2,
594 accept_if_possible: bool,
595) -> bool {
596 let mut accepted = false;
597 if accept_if_possible {
598 let drop_target = dnd_target_id_at(frame, pos);
599 if let Some(tid) = drop_target {
600 if let Some(i) = hit_index_by_id(frame, tid) {
601 if let Some(cb) = &frame.hit_regions[i].on_drop {
602 accepted = cb(DropEvent {
603 source_id: session.source_id,
604 target_id: tid,
605 position: pos,
606 modifiers,
607 payload: session.payload.clone(),
608 });
609 }
610 }
611 }
612 }
613
614 if let Some(i) = hit_index_by_id(frame, session.source_id) {
615 if let Some(cb) = &frame.hit_regions[i].on_drag_end {
616 cb(repose_core::dnd::DragEnd { accepted });
617 }
618 }
619
620 accepted
621}
622
623#[derive(Clone)]
624pub(crate) struct DragSession {
625 pub source_id: u64,
626 pub payload: DragPayload,
627 pub start_px: (f32, f32),
628 pub over_id: Option<u64>,
629}