1use crate::*;
2use repose_core::Modifiers;
3use repose_core::Vec2;
4use repose_core::input::{PointerButton, PointerEvent, PointerEventKind, PointerId, PointerKind};
5use repose_core::locals::dp_to_px;
6use repose_core::runtime::Frame;
7use repose_ui::TextFieldState;
8use repose_ui::textfield::{
9 TF_FONT_DP, TextMeasureConfig, caret_xy_for_byte, index_for_x_bytes, index_for_xy_bytes, measure_text,
10};
11
12pub(crate) fn tick_snackbar(last_redraw: web_time::Instant) {
13 let now = web_time::Instant::now();
14 let elapsed = now.saturating_duration_since(last_redraw);
15 let ms = elapsed.as_millis().min(u32::MAX as u128) as u32;
16 if ms > 0 {
17 repose_ui::overlay::SnackbarController::tick_for_frame(ms);
18 }
19}
20
21pub(crate) fn request_redraw(window: &Option<std::sync::Arc<winit::window::Window>>) {
22 if let Some(w) = window {
23 w.request_redraw();
24 }
25}
26
27pub(crate) fn tf_key_of_in_frame(frame_cache: &Option<Frame>, visual_id: u64) -> u64 {
28 if let Some(f) = frame_cache {
29 return tf_key_of(f, visual_id);
30 }
31 visual_id
32}
33
34pub(crate) fn is_textfield_in_frame(frame_cache: &Option<Frame>, id: u64) -> bool {
35 if let Some(f) = frame_cache {
36 f.semantics_nodes
37 .iter()
38 .any(|n| n.id == id && n.role == Role::TextField)
39 } else {
40 false
41 }
42}
43
44pub(crate) fn update_modifiers(modifiers: &mut Modifiers, state: &winit::keyboard::ModifiersState) {
45 modifiers.shift = state.shift_key();
46 modifiers.ctrl = state.control_key();
47 modifiers.alt = state.alt_key();
48 modifiers.meta = state.super_key();
49 modifiers.command = if cfg!(target_os = "macos") {
50 modifiers.meta
51 } else {
52 modifiers.ctrl
53 };
54}
55
56pub(crate) fn index_for_x_bytes_vt(state: &TextFieldState, font_px: f32, x_px: f32) -> usize {
59 if let Some(vt) = &state.visual_transformation {
60 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
61 let tfmd = vt.filter(&annotated);
62 let display_idx = index_for_x_bytes(tfmd.text.as_str(), font_px, x_px, 400, 0);
63 tfmd.offset_mapping.transformed_to_original(display_idx)
64 } else {
65 index_for_x_bytes(&state.text, font_px, x_px, 400, 0)
66 }
67}
68
69pub(crate) fn index_for_xy_bytes_vt(
71 state: &TextFieldState,
72 font_px: f32,
73 wrap_w: f32,
74 x_px: f32,
75 y_px: f32,
76) -> usize {
77 if let Some(vt) = &state.visual_transformation {
78 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
79 let tfmd = vt.filter(&annotated);
80 let display_idx = index_for_xy_bytes(tfmd.text.as_str(), font_px, wrap_w, x_px, y_px);
81 tfmd.offset_mapping.transformed_to_original(display_idx)
82 } else {
83 index_for_xy_bytes(&state.text, font_px, wrap_w, x_px, y_px)
84 }
85}
86
87pub(crate) fn top_hit_index(frame: &Frame, pos: Vec2) -> Option<usize> {
89 frame
90 .hit_regions
91 .iter()
92 .enumerate()
93 .rev()
94 .find(|(_, h)| h.rect.contains(pos))
95 .map(|(i, _)| i)
96}
97
98pub(crate) fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
99 frame.hit_regions.iter().position(|h| h.id == id)
100}
101
102pub(crate) fn tf_key_of(frame: &Frame, visual_id: u64) -> u64 {
103 if let Some(i) = hit_index_by_id(frame, visual_id) {
104 let hr = &frame.hit_regions[i];
105 return hr.tf_state_key.unwrap_or(hr.id);
106 }
107 visual_id
108}
109
110pub(crate) fn pe_mouse(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
111 PointerEvent::new(PointerId(0), PointerKind::Mouse, event, pos, 1.0, mods)
112}
113
114pub(crate) fn pe_touch(event: PointerEventKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
115 PointerEvent::new(PointerId(0), PointerKind::Touch, event, pos, 1.0, mods)
116}
117
118pub(crate) fn pe_down_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
119 PointerEvent::new(
120 PointerId(0),
121 kind,
122 PointerEventKind::Down(PointerButton::Primary),
123 pos,
124 1.0,
125 mods,
126 )
127}
128
129pub(crate) fn pe_up_primary(kind: PointerKind, pos: Vec2, mods: Modifiers) -> PointerEvent {
130 PointerEvent::new(
131 PointerId(0),
132 kind,
133 PointerEventKind::Up(PointerButton::Primary),
134 pos,
135 1.0,
136 mods,
137 )
138}
139
140pub(crate) fn map_key(key: winit::keyboard::PhysicalKey) -> repose_core::input::Key {
141 use repose_core::input::Key;
142 use winit::keyboard::{KeyCode, PhysicalKey};
143
144 match key {
145 PhysicalKey::Code(KeyCode::Enter) => Key::Enter,
146 PhysicalKey::Code(KeyCode::Tab) => Key::Tab,
147 PhysicalKey::Code(KeyCode::Backspace) => Key::Backspace,
148 PhysicalKey::Code(KeyCode::Delete) => Key::Delete,
149 PhysicalKey::Code(KeyCode::Escape) => Key::Escape,
150 PhysicalKey::Code(KeyCode::ArrowLeft) => Key::ArrowLeft,
151 PhysicalKey::Code(KeyCode::ArrowRight) => Key::ArrowRight,
152 PhysicalKey::Code(KeyCode::ArrowUp) => Key::ArrowUp,
153 PhysicalKey::Code(KeyCode::ArrowDown) => Key::ArrowDown,
154 PhysicalKey::Code(KeyCode::Home) => Key::Home,
155 PhysicalKey::Code(KeyCode::End) => Key::End,
156 PhysicalKey::Code(KeyCode::PageUp) => Key::PageUp,
157 PhysicalKey::Code(KeyCode::PageDown) => Key::PageDown,
158 PhysicalKey::Code(KeyCode::Space) => Key::Space,
159 PhysicalKey::Code(KeyCode::KeyA) => Key::Character('a'),
160 PhysicalKey::Code(KeyCode::KeyB) => Key::Character('b'),
161 PhysicalKey::Code(KeyCode::KeyC) => Key::Character('c'),
162 PhysicalKey::Code(KeyCode::KeyD) => Key::Character('d'),
163 PhysicalKey::Code(KeyCode::KeyE) => Key::Character('e'),
164 PhysicalKey::Code(KeyCode::KeyF) => Key::Character('f'),
165 PhysicalKey::Code(KeyCode::KeyG) => Key::Character('g'),
166 PhysicalKey::Code(KeyCode::KeyH) => Key::Character('h'),
167 PhysicalKey::Code(KeyCode::KeyI) => Key::Character('i'),
168 PhysicalKey::Code(KeyCode::KeyJ) => Key::Character('j'),
169 PhysicalKey::Code(KeyCode::KeyK) => Key::Character('k'),
170 PhysicalKey::Code(KeyCode::KeyL) => Key::Character('l'),
171 PhysicalKey::Code(KeyCode::KeyM) => Key::Character('m'),
172 PhysicalKey::Code(KeyCode::KeyN) => Key::Character('n'),
173 PhysicalKey::Code(KeyCode::KeyO) => Key::Character('o'),
174 PhysicalKey::Code(KeyCode::KeyP) => Key::Character('p'),
175 PhysicalKey::Code(KeyCode::KeyQ) => Key::Character('q'),
176 PhysicalKey::Code(KeyCode::KeyR) => Key::Character('r'),
177 PhysicalKey::Code(KeyCode::KeyS) => Key::Character('s'),
178 PhysicalKey::Code(KeyCode::KeyT) => Key::Character('t'),
179 PhysicalKey::Code(KeyCode::KeyU) => Key::Character('u'),
180 PhysicalKey::Code(KeyCode::KeyV) => Key::Character('v'),
181 PhysicalKey::Code(KeyCode::KeyW) => Key::Character('w'),
182 PhysicalKey::Code(KeyCode::KeyX) => Key::Character('x'),
183 PhysicalKey::Code(KeyCode::KeyY) => Key::Character('y'),
184 PhysicalKey::Code(KeyCode::KeyZ) => Key::Character('z'),
185 PhysicalKey::Code(KeyCode::Digit0) => Key::Character('0'),
186 PhysicalKey::Code(KeyCode::Digit1) => Key::Character('1'),
187 PhysicalKey::Code(KeyCode::Digit2) => Key::Character('2'),
188 PhysicalKey::Code(KeyCode::Digit3) => Key::Character('3'),
189 PhysicalKey::Code(KeyCode::Digit4) => Key::Character('4'),
190 PhysicalKey::Code(KeyCode::Digit5) => Key::Character('5'),
191 PhysicalKey::Code(KeyCode::Digit6) => Key::Character('6'),
192 PhysicalKey::Code(KeyCode::Digit7) => Key::Character('7'),
193 PhysicalKey::Code(KeyCode::Digit8) => Key::Character('8'),
194 PhysicalKey::Code(KeyCode::Digit9) => Key::Character('9'),
195 PhysicalKey::Code(KeyCode::F1) => Key::F(1),
196 PhysicalKey::Code(KeyCode::F2) => Key::F(2),
197 PhysicalKey::Code(KeyCode::F3) => Key::F(3),
198 PhysicalKey::Code(KeyCode::F4) => Key::F(4),
199 PhysicalKey::Code(KeyCode::F5) => Key::F(5),
200 PhysicalKey::Code(KeyCode::F6) => Key::F(6),
201 PhysicalKey::Code(KeyCode::F7) => Key::F(7),
202 PhysicalKey::Code(KeyCode::F8) => Key::F(8),
203 PhysicalKey::Code(KeyCode::F9) => Key::F(9),
204 PhysicalKey::Code(KeyCode::F10) => Key::F(10),
205 PhysicalKey::Code(KeyCode::F11) => Key::F(11),
206 PhysicalKey::Code(KeyCode::F12) => Key::F(12),
207 _ => Key::Unknown,
208 }
209}
210
211pub(crate) fn tf_ensure_caret_visible(state: &mut TextFieldState, is_multiline: bool) {
212 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
213 let wrap_width = state.inner_width;
214
215 if is_multiline {
216 let (cx, cy, _) = caret_xy_for_byte(&state.text, font_px, wrap_width, state.caret_index());
217 let iw = state.inner_width;
218 let ih = state.inner_height;
219 state.ensure_caret_visible_xy(cx, cy, iw, ih, dp_to_px(2.0));
220 } else {
221 let caret_idx = state.caret_index();
222 let (display, caret_display_off) = if let Some(vt) = &state.visual_transformation {
223 let annotated = repose_core::AnnotatedString::new(state.text.clone(), vec![]);
224 let tfmd = vt.filter(&annotated);
225 let off =
226 repose_core::original_offset_to_display(&state.text, tfmd.text.as_str(), caret_idx);
227 (tfmd.text.text, off)
228 } else {
229 (state.text.clone(), caret_idx)
230 };
231 let m = measure_text(&display, font_px, TextMeasureConfig::default());
232 let caret_x_px = m.positions.get(caret_display_off).copied().unwrap_or(0.0);
233 state.ensure_caret_visible(caret_x_px, wrap_width, dp_to_px(2.0));
234 }
235}
236
237pub(crate) fn tf_place_caret_at_pointer(
243 state: &mut TextFieldState,
244 hit_rect: Rect,
245 content_origin: Option<(f32, f32)>,
246 is_multiline: bool,
247 pos_px: (f32, f32),
248 scale: f32,
249 shift: bool,
250) {
251 let (ox, oy) = content_origin.unwrap_or((hit_rect.x, hit_rect.y));
252 let content_x_px = (pos_px.0 - ox + state.scroll_offset).max(0.0);
253 let content_y_px = (pos_px.1 - oy + state.scroll_offset_y).max(0.0);
254 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
255 let wrap_w = state.inner_width.max(1.0);
256
257 let idx = if is_multiline {
258 index_for_xy_bytes_vt(state, font_px, wrap_w, content_x_px, content_y_px)
259 } else {
260 index_for_x_bytes_vt(state, font_px, content_x_px)
261 };
262 state.handle_pointer_down(idx, (pos_px.0, pos_px.1), shift);
263}
264
265pub(crate) fn dispatch_scroll(
271 frame: &Frame,
272 pos: Vec2,
273 delta: Vec2,
274 scroll_capture: Option<u64>,
275) -> (bool, Option<u64>) {
276 if let Some(cid) = scroll_capture {
277 if let Some(cb) = frame
278 .hit_regions
279 .iter()
280 .find(|h| h.id == cid)
281 .and_then(|h| h.on_scroll.as_ref())
282 {
283 cb(delta);
284 return (true, Some(cid));
285 }
286 }
288
289 let mut remaining = delta;
292 for hit in frame
293 .hit_regions
294 .iter()
295 .rev()
296 .filter(|h| h.rect.contains(pos))
297 {
298 if let Some(cb) = &hit.on_scroll {
299 let before = remaining;
300 let leftover = cb(before);
301 let consumed = (before.x - leftover.x).abs() > 0.001
302 || (before.y - leftover.y).abs() > 0.001;
303 if consumed {
304 return (true, Some(hit.id));
305 }
306 remaining = leftover;
307 if remaining.x.abs() <= 0.001 && remaining.y.abs() <= 0.001 {
308 break;
309 }
310 }
311 }
312 (false, scroll_capture)
313}
314
315#[macro_export]
316macro_rules! handle_text_undo_redo {
317 ($app:expr, $key_event:expr) => {{
318 let mut __handled = false;
319 if $key_event.state == ElementState::Pressed && !$key_event.repeat && $app.modifiers.command
320 {
321 match $key_event.physical_key {
322 PhysicalKey::Code(KeyCode::KeyZ) if $app.modifiers.shift => {
323 if let Some(fid) = $app.sched.focused {
324 let key = $app.tf_key_of(fid);
325 if let Some(state_rc) = $app.textfield_states.get(&key) {
326 let mut st = state_rc.borrow_mut();
327 if st.can_redo() {
328 st.redo();
329 $app.notify_text_change(fid, st.text.clone());
330 __handled = true;
331 }
332 }
333 }
334 }
335 PhysicalKey::Code(KeyCode::KeyZ) => {
336 if let Some(fid) = $app.sched.focused {
337 let key = $app.tf_key_of(fid);
338 if let Some(state_rc) = $app.textfield_states.get(&key) {
339 let mut st = state_rc.borrow_mut();
340 if st.can_undo() {
341 st.undo();
342 $app.notify_text_change(fid, st.text.clone());
343 __handled = true;
344 }
345 }
346 }
347 }
348 _ => {}
349 }
350 }
351 __handled
352 }};
353}
354
355pub(crate) fn process_render_commands(
356 backend: &mut repose_render_wgpu::WgpuBackend,
357 cmds: Vec<RenderCommand>,
358) {
359 for cmd in cmds {
360 match cmd {
361 RenderCommand::SetImageEncoded {
362 handle,
363 bytes,
364 srgb,
365 } => {
366 let _ = backend.set_image_from_bytes(handle, &bytes, srgb);
367 }
368 RenderCommand::SetImageRgba8 {
369 handle,
370 w,
371 h,
372 rgba,
373 srgb,
374 } => {
375 let _ = backend.set_image_rgba8(handle, w, h, &rgba, srgb);
376 }
377 RenderCommand::SetImageNv12 {
378 handle,
379 w,
380 h,
381 y,
382 uv,
383 color_info,
384 } => {
385 let _ = backend.set_image_nv12(handle, w, h, &y, &uv, color_info);
386 }
387 RenderCommand::SetImagePlanes {
388 handle,
389 w,
390 h,
391 pixel_format,
392 planes,
393 color_info,
394 } => {
395 let refs: Vec<&[u8]> = planes.iter().map(|p| p.as_ref()).collect();
396 let _ = backend.set_image_planes(handle, w, h, pixel_format, &refs, color_info);
397 }
398 #[cfg(target_os = "linux")]
399 RenderCommand::SetImageDmaBuf {
400 handle,
401 w,
402 h,
403 fds,
404 fourcc: _,
405 modifier,
406 strides,
407 offsets,
408 color_info,
409 } => {
410 if let Err(e) = backend.set_image_dmabuf(handle, w, h, fds, modifier, strides, offsets, color_info) {
411 log::warn!("set_image_dmabuf failed: {e:?}");
412 }
413 }
414 RenderCommand::RemoveImage { handle } => {
415 backend.remove_image(handle);
416 }
417 }
418 }
419}