#[allow(unused_imports)]
pub use repose_app::MultiTouchDelta;
pub use repose_app::TouchGestureState;
use repose_core::input::Modifiers;
use repose_core::runtime::Frame;
use repose_core::{RenderBackend, Vec2};
pub(crate) struct FramePacer {
last_render: Option<web_time::Instant>,
max_fps: Option<f32>,
}
impl FramePacer {
pub(crate) fn new(max_fps: Option<f32>) -> Self {
Self {
last_render: None,
max_fps,
}
}
pub(crate) fn due(&self, now: web_time::Instant) -> bool {
self.deadline(now).is_none()
}
pub(crate) fn deadline(&self, now: web_time::Instant) -> Option<web_time::Instant> {
let interval = {
let fps = self.max_fps.filter(|fps| fps.is_finite() && *fps > 0.0)?;
web_time::Duration::from_secs_f64((1.0 / f64::from(fps)).clamp(0.0, 1.0))
};
let deadline = self.last_render.map(|last| last + interval)?;
(deadline > now).then_some(deadline)
}
pub(crate) fn rendered(&mut self, now: web_time::Instant) {
self.last_render = Some(now);
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub(crate) fn idle_due(&self, now: web_time::Instant, idle: web_time::Duration) -> bool {
self.last_render
.is_none_or(|last| now.saturating_duration_since(last) >= idle)
}
}
#[cfg(not(target_arch = "wasm32"))]
enum ClipboardWrite {
System(String),
Primary(String),
}
#[cfg(not(target_arch = "wasm32"))]
struct ClipboardWriteState {
next: u64,
completed: u64,
}
#[cfg(not(target_arch = "wasm32"))]
struct ClipboardSync {
state: std::sync::Mutex<ClipboardWriteState>,
idle: std::sync::Condvar,
}
#[cfg(not(target_arch = "wasm32"))]
struct ClipboardWriter {
sender: std::sync::mpsc::Sender<(u64, ClipboardWrite)>,
sync: std::sync::Arc<ClipboardSync>,
}
#[cfg(not(target_arch = "wasm32"))]
impl ClipboardWriter {
fn enqueue(&self, write: ClipboardWrite) {
let failed = {
let mut state = self
.sync
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
state.next = state.next.wrapping_add(1);
let sequence = state.next;
if self.sender.send((sequence, write)).is_err() {
state.completed = state.completed.max(sequence);
true
} else {
false
}
};
if failed {
self.sync.idle.notify_all();
log::warn!("clipboard writer is unavailable");
}
}
fn wait(&self) {
let mut state = self
.sync
.state
.lock()
.unwrap_or_else(|error| error.into_inner());
while state.completed < state.next {
state = self
.sync
.idle
.wait(state)
.unwrap_or_else(|error| error.into_inner());
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl ClipboardSync {
fn complete(&self, sequence: u64) {
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
state.completed = state.completed.max(sequence);
self.idle.notify_all();
}
}
#[cfg(not(target_arch = "wasm32"))]
fn clipboard_writes() -> &'static ClipboardWriter {
static WRITES: std::sync::OnceLock<ClipboardWriter> = std::sync::OnceLock::new();
WRITES.get_or_init(|| {
let (sender, receiver) = std::sync::mpsc::channel();
let sync = std::sync::Arc::new(ClipboardSync {
state: std::sync::Mutex::new(ClipboardWriteState {
next: 0,
completed: 0,
}),
idle: std::sync::Condvar::new(),
});
let writer_sync = sync.clone();
let _ = std::thread::Builder::new()
.name("repose-clipboard-writes".into())
.spawn(move || {
let system_clipboard = match clipawl::Clipboard::new() {
Ok(clipboard) => Some(clipboard),
Err(error) => {
log::warn!("clipboard initialization failed: {error}");
None
}
};
let primary_clipboard = {
let mut opts = clipawl::ClipboardOptions::default();
opts.linux.selection = clipawl::LinuxSelection::Primary;
match clipawl::Clipboard::new_with_options(opts) {
Ok(clipboard) => Some(clipboard),
Err(error) => {
log::warn!("primary clipboard initialization failed: {error}");
None
}
}
};
for (sequence, write) in receiver {
match write {
ClipboardWrite::System(text) => {
if let Some(clipboard) = system_clipboard.as_ref()
&& let Err(error) = pollster::block_on(clipboard.write(&text))
{
log::warn!("clipboard write failed: {error}");
}
}
ClipboardWrite::Primary(text) => {
if let Some(clipboard) = primary_clipboard.as_ref()
&& let Err(error) = pollster::block_on(clipboard.write(&text))
{
log::warn!("primary selection write failed: {error}");
}
}
}
writer_sync.complete(sequence);
}
});
ClipboardWriter { sender, sync }
})
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn flush_clipboard_writes() {
clipboard_writes().wait();
}
pub(crate) fn request_redraw(window: &Option<std::sync::Arc<winit::window::Window>>) {
if let Some(w) = window {
w.request_redraw();
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn setup_clipboard() -> Option<clipawl::Clipboard> {
let cb = clipawl::Clipboard::new().ok();
repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
flush_clipboard_writes();
clipawl::blocking::read().ok()
}));
repose_core::clipboard::set_clipboard_fn(Box::new(|text| {
clipboard_writes().enqueue(ClipboardWrite::System(text.to_string()));
}));
repose_core::clipboard::set_primary_fn(Box::new(|text| {
clipboard_writes().enqueue(ClipboardWrite::Primary(text.to_string()));
}));
cb
}
pub fn sync_viewport(
rt: &mut repose_app::ReposeRuntime,
backend: &mut Option<repose_render_wgpu::WgpuBackend>,
size: winit::dpi::PhysicalSize<u32>,
scale: f32,
) -> f32 {
let (width, height) = match backend.as_mut() {
Some(b) => b.configure_surface(size.width, size.height),
None => (size.width, size.height),
};
let fitted = if width == 0 || size.width == 0 {
1.0
} else {
width as f32 / size.width as f32
};
rt.set_viewport_and_scale(width, height, scale * fitted);
if let Some(b) = backend.as_mut() {
b.set_pixels_per_point(scale * fitted);
}
fitted
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
pub fn sync_ime_for_focused(
window: &winit::window::Window,
rt: &repose_app::ReposeRuntime,
frame: &repose_core::runtime::Frame,
) {
if !rt.sched.window_focused {
set_ime_for_textfield(window, false);
return;
}
let Some(fid) = rt.sched.focused else {
set_ime_for_textfield(window, false);
return;
};
if !is_editable_textfield_hit(frame, fid) {
set_ime_for_textfield(window, false);
return;
}
let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid) else {
set_ime_for_textfield(window, false);
return;
};
set_ime_for_textfield_ex(
window,
true,
hit.keyboard_type.ime_purpose_hint(),
hit.auto_correct.unwrap_or(true),
hit.capitalization,
);
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
{
let sf = window.scale_factor();
window.set_ime_cursor_area(
winit::dpi::LogicalPosition::new(hit.rect.x as f64 / sf, hit.rect.y as f64 / sf),
winit::dpi::LogicalSize::new(hit.rect.w as f64 / sf, hit.rect.h as f64 / sf),
);
}
}
#[allow(dead_code)]
pub fn needs_compose(
dirty: bool,
frame_requested: bool,
present_requested: bool,
has_frame: bool,
continuous: bool,
animation_active: bool,
wakeup_due: bool,
) -> bool {
continuous
|| dirty
|| frame_requested
|| (present_requested && has_frame)
|| animation_active
|| wakeup_due
}
#[allow(dead_code)]
pub(crate) fn is_textfield_in_frame(frame_cache: &Option<std::rc::Rc<Frame>>, id: u64) -> bool {
repose_app::is_textfield_in_frame_cache(frame_cache, id)
}
#[allow(dead_code)]
pub fn dispatch_file_drop(
frame: &Frame,
pos: Vec2,
paths: Vec<std::path::PathBuf>,
modifiers: Modifiers,
) -> bool {
let mut files = Vec::new();
for p in paths {
let name = p
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("file")
.to_string();
files.push(repose_core::dnd::DroppedFile {
name,
path: Some(p),
});
}
let payload: repose_core::dnd::DragPayload =
std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
let Some(target_id) = repose_core::dnd::dnd_target_id_at(frame, pos) else {
return false;
};
let Some(hit) = frame.hit_regions.iter().find(|h| h.id == target_id) else {
return false;
};
let Some(cb) = &hit.on_drop else {
return false;
};
cb(repose_core::dnd::DropEvent {
source_id: 0,
target_id,
position: pos,
modifiers,
payload,
});
true
}
pub(crate) fn update_modifiers(modifiers: &mut Modifiers, state: &winit::keyboard::ModifiersState) {
modifiers.shift = state.shift_key();
modifiers.ctrl = state.control_key();
modifiers.alt = state.alt_key();
modifiers.meta = state.super_key();
modifiers.command = if cfg!(target_os = "macos") {
modifiers.meta
} else {
modifiers.ctrl
};
}
pub(crate) fn is_editable_hit(hit: &repose_core::HitRegion) -> bool {
hit.tf_enabled && !hit.tf_read_only && hit.tf_state_key.is_some()
}
pub(crate) fn editable_textfield_hit<'a>(
hits: &'a [repose_core::HitRegion],
semantics: &[repose_core::runtime::SemNode],
id: u64,
) -> Option<&'a repose_core::HitRegion> {
let hit = hits.iter().find(|hit| hit.id == id)?;
(is_editable_hit(hit)
&& semantics
.iter()
.any(|node| node.id == id && node.role == repose_core::semantics::Role::TextField))
.then_some(hit)
}
pub(crate) fn is_editable_textfield_hit(frame: &Frame, id: u64) -> bool {
editable_textfield_hit(&frame.hit_regions, &frame.semantics_nodes, id).is_some()
}
pub(crate) fn has_ime_handler_hit(hits: &[repose_core::HitRegion], id: u64) -> bool {
hits.iter()
.any(|hit| hit.id == id && !hit.disabled && hit.on_ime.is_some())
}
pub(crate) fn has_ime_handler(frame: &Frame, id: u64) -> bool {
has_ime_handler_hit(&frame.hit_regions, id)
}
#[allow(dead_code)]
pub(crate) fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
repose_app::hit_index_by_id(frame, id)
}
pub(crate) fn is_back_key(event: &winit::event::KeyEvent) -> bool {
event.logical_key == winit::keyboard::Key::Named(winit::keyboard::NamedKey::BrowserBack)
|| is_back_key_physical(event.physical_key)
}
pub(crate) fn is_back_key_physical(key: winit::keyboard::PhysicalKey) -> bool {
if matches!(
key,
winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::BrowserBack)
) {
return true;
}
#[cfg(target_os = "android")]
if matches!(
key,
winit::keyboard::PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Android(4))
) {
return true;
}
false
}
pub(crate) fn is_escape_key(event: &winit::event::KeyEvent) -> bool {
event.logical_key == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape)
|| matches!(
event.physical_key,
winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Escape)
)
}
pub(crate) fn winit_key_to_repose(
physical_key: winit::keyboard::PhysicalKey,
pressed: bool,
repeat: bool,
mapped_key: &repose_core::input::Key,
mods: &repose_core::input::Modifiers,
) -> repose_core::input::KeyEvent {
let utf16 = match mapped_key {
repose_core::input::Key::Character(c) => *c as u16,
repose_core::input::Key::ShiftLeft | repose_core::input::Key::ShiftRight => 0,
_ => 0,
};
repose_core::input::KeyEvent {
key: mapped_key.clone(),
modifiers: *mods,
is_repeat: repeat,
event_type: if pressed {
repose_core::input::KeyEventType::Down
} else {
repose_core::input::KeyEventType::Up
},
utf16_code_point: utf16,
physical: Some(crate::runner_common::map_physical_key(physical_key)),
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub(crate) fn map_cursor(c: &repose_core::CursorIcon) -> winit::window::Cursor {
use winit::window::{Cursor, CursorIcon as W};
match c {
repose_core::CursorIcon::Hidden => Cursor::Icon(W::Default),
repose_core::CursorIcon::Default => Cursor::Icon(W::Default),
repose_core::CursorIcon::Pointer => Cursor::Icon(W::Pointer),
repose_core::CursorIcon::Text => Cursor::Icon(W::Text),
repose_core::CursorIcon::EwResize => Cursor::Icon(W::EwResize),
repose_core::CursorIcon::NsResize => Cursor::Icon(W::NsResize),
repose_core::CursorIcon::NwseResize => Cursor::Icon(W::NwseResize),
repose_core::CursorIcon::NeswResize => Cursor::Icon(W::NeswResize),
repose_core::CursorIcon::Grab => Cursor::Icon(W::Grab),
repose_core::CursorIcon::Grabbing => Cursor::Icon(W::Grabbing),
repose_core::CursorIcon::Custom(_) => Cursor::Icon(W::Default),
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub(crate) fn cursor_is_hidden(c: &repose_core::CursorIcon) -> bool {
matches!(c, repose_core::CursorIcon::Hidden)
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn cursor_css(c: &repose_core::CursorIcon) -> &'static str {
match c {
repose_core::CursorIcon::Hidden => "none",
repose_core::CursorIcon::Default => "default",
repose_core::CursorIcon::Pointer => "pointer",
repose_core::CursorIcon::Text => "text",
repose_core::CursorIcon::EwResize => "ew-resize",
repose_core::CursorIcon::NsResize => "ns-resize",
repose_core::CursorIcon::NwseResize => "nwse-resize",
repose_core::CursorIcon::NeswResize => "nesw-resize",
repose_core::CursorIcon::Grab => "grab",
repose_core::CursorIcon::Grabbing => "grabbing",
repose_core::CursorIcon::Custom(_) => "default",
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn map_ime_purpose(hint: repose_core::ImePurposeHint) -> winit::window::ImePurpose {
match hint {
repose_core::ImePurposeHint::Password => winit::window::ImePurpose::Password,
_ => winit::window::ImePurpose::Normal,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_ime_for_textfield(window: &winit::window::Window, is_textfield: bool) {
set_ime_for_textfield_ex(
window,
is_textfield,
repose_core::ImePurposeHint::Normal,
true,
repose_core::KeyboardCapitalization::Unspecified,
);
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_ime_for_textfield_ex(
window: &winit::window::Window,
is_textfield: bool,
_purpose: repose_core::ImePurposeHint,
_auto_correct: bool,
_capitalization: repose_core::KeyboardCapitalization,
) {
if is_textfield {
window.set_ime_allowed(true);
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
window.set_ime_purpose(map_ime_purpose(_purpose));
} else {
window.set_ime_allowed(false);
}
}
pub(crate) fn map_key(
key: winit::keyboard::PhysicalKey,
mods: &repose_core::input::Modifiers,
) -> repose_core::input::Key {
use repose_core::input::Key;
use winit::keyboard::{KeyCode, PhysicalKey};
let shifted = |a: char, b: char| Key::Character(if mods.shift { b } else { a });
match key {
PhysicalKey::Code(KeyCode::Enter) => Key::Enter,
PhysicalKey::Code(KeyCode::Tab) => Key::Tab,
PhysicalKey::Code(KeyCode::Backspace) => Key::Backspace,
PhysicalKey::Code(KeyCode::Delete) => Key::Delete,
PhysicalKey::Code(KeyCode::Insert) => Key::Insert,
PhysicalKey::Code(KeyCode::Escape) | PhysicalKey::Code(KeyCode::BrowserBack) => Key::Escape,
PhysicalKey::Code(KeyCode::ArrowLeft) => Key::ArrowLeft,
PhysicalKey::Code(KeyCode::ArrowRight) => Key::ArrowRight,
PhysicalKey::Code(KeyCode::ArrowUp) => Key::ArrowUp,
PhysicalKey::Code(KeyCode::ArrowDown) => Key::ArrowDown,
PhysicalKey::Code(KeyCode::Home) => Key::Home,
PhysicalKey::Code(KeyCode::End) => Key::End,
PhysicalKey::Code(KeyCode::PageUp) => Key::PageUp,
PhysicalKey::Code(KeyCode::PageDown) => Key::PageDown,
PhysicalKey::Code(KeyCode::Space) => Key::Space,
PhysicalKey::Code(KeyCode::ShiftLeft) => Key::ShiftLeft,
PhysicalKey::Code(KeyCode::ShiftRight) => Key::ShiftRight,
PhysicalKey::Code(KeyCode::KeyA) => Key::Character('a'),
PhysicalKey::Code(KeyCode::KeyB) => Key::Character('b'),
PhysicalKey::Code(KeyCode::KeyC) => Key::Character('c'),
PhysicalKey::Code(KeyCode::KeyD) => Key::Character('d'),
PhysicalKey::Code(KeyCode::KeyE) => Key::Character('e'),
PhysicalKey::Code(KeyCode::KeyF) => Key::Character('f'),
PhysicalKey::Code(KeyCode::KeyG) => Key::Character('g'),
PhysicalKey::Code(KeyCode::KeyH) => Key::Character('h'),
PhysicalKey::Code(KeyCode::KeyI) => Key::Character('i'),
PhysicalKey::Code(KeyCode::KeyJ) => Key::Character('j'),
PhysicalKey::Code(KeyCode::KeyK) => Key::Character('k'),
PhysicalKey::Code(KeyCode::KeyL) => Key::Character('l'),
PhysicalKey::Code(KeyCode::KeyM) => Key::Character('m'),
PhysicalKey::Code(KeyCode::KeyN) => Key::Character('n'),
PhysicalKey::Code(KeyCode::KeyO) => Key::Character('o'),
PhysicalKey::Code(KeyCode::KeyP) => Key::Character('p'),
PhysicalKey::Code(KeyCode::KeyQ) => Key::Character('q'),
PhysicalKey::Code(KeyCode::KeyR) => Key::Character('r'),
PhysicalKey::Code(KeyCode::KeyS) => Key::Character('s'),
PhysicalKey::Code(KeyCode::KeyT) => Key::Character('t'),
PhysicalKey::Code(KeyCode::KeyU) => Key::Character('u'),
PhysicalKey::Code(KeyCode::KeyV) => Key::Character('v'),
PhysicalKey::Code(KeyCode::KeyW) => Key::Character('w'),
PhysicalKey::Code(KeyCode::KeyX) => Key::Character('x'),
PhysicalKey::Code(KeyCode::KeyY) => Key::Character('y'),
PhysicalKey::Code(KeyCode::KeyZ) => Key::Character('z'),
PhysicalKey::Code(KeyCode::Digit0) => shifted('0', ')'),
PhysicalKey::Code(KeyCode::Digit1) => shifted('1', '!'),
PhysicalKey::Code(KeyCode::Digit2) => shifted('2', '@'),
PhysicalKey::Code(KeyCode::Digit3) => shifted('3', '#'),
PhysicalKey::Code(KeyCode::Digit4) => shifted('4', '$'),
PhysicalKey::Code(KeyCode::Digit5) => shifted('5', '%'),
PhysicalKey::Code(KeyCode::Digit6) => shifted('6', '^'),
PhysicalKey::Code(KeyCode::Digit7) => shifted('7', '&'),
PhysicalKey::Code(KeyCode::Digit8) => shifted('8', '*'),
PhysicalKey::Code(KeyCode::Digit9) => shifted('9', '('),
PhysicalKey::Code(KeyCode::Minus) => shifted('-', '_'),
PhysicalKey::Code(KeyCode::Equal) => shifted('=', '+'),
PhysicalKey::Code(KeyCode::BracketLeft) => shifted('[', '{'),
PhysicalKey::Code(KeyCode::BracketRight) => shifted(']', '}'),
PhysicalKey::Code(KeyCode::Backslash) => shifted('\\', '|'),
PhysicalKey::Code(KeyCode::Semicolon) => shifted(';', ':'),
PhysicalKey::Code(KeyCode::Quote) => shifted('\'', '"'),
PhysicalKey::Code(KeyCode::Comma) => shifted(',', '<'),
PhysicalKey::Code(KeyCode::Period) => shifted('.', '>'),
PhysicalKey::Code(KeyCode::Slash) => shifted('/', '?'),
PhysicalKey::Code(KeyCode::Backquote) => shifted('`', '~'),
PhysicalKey::Code(KeyCode::NumpadAdd) => Key::Character('+'),
PhysicalKey::Code(KeyCode::NumpadSubtract) => Key::Character('-'),
PhysicalKey::Code(KeyCode::NumpadMultiply) => Key::Character('*'),
PhysicalKey::Code(KeyCode::NumpadDivide) => Key::Character('/'),
PhysicalKey::Code(KeyCode::NumpadEnter) => Key::Enter,
PhysicalKey::Code(KeyCode::NumpadDecimal) => Key::Character('.'),
PhysicalKey::Code(KeyCode::Numpad0) => Key::Character('0'),
PhysicalKey::Code(KeyCode::Numpad1) => Key::Character('1'),
PhysicalKey::Code(KeyCode::Numpad2) => Key::Character('2'),
PhysicalKey::Code(KeyCode::Numpad3) => Key::Character('3'),
PhysicalKey::Code(KeyCode::Numpad4) => Key::Character('4'),
PhysicalKey::Code(KeyCode::Numpad5) => Key::Character('5'),
PhysicalKey::Code(KeyCode::Numpad6) => Key::Character('6'),
PhysicalKey::Code(KeyCode::Numpad7) => Key::Character('7'),
PhysicalKey::Code(KeyCode::Numpad8) => Key::Character('8'),
PhysicalKey::Code(KeyCode::Numpad9) => Key::Character('9'),
PhysicalKey::Code(KeyCode::F1) => Key::F(1),
PhysicalKey::Code(KeyCode::F2) => Key::F(2),
PhysicalKey::Code(KeyCode::F3) => Key::F(3),
PhysicalKey::Code(KeyCode::F4) => Key::F(4),
PhysicalKey::Code(KeyCode::F5) => Key::F(5),
PhysicalKey::Code(KeyCode::F6) => Key::F(6),
PhysicalKey::Code(KeyCode::F7) => Key::F(7),
PhysicalKey::Code(KeyCode::F8) => Key::F(8),
PhysicalKey::Code(KeyCode::F9) => Key::F(9),
PhysicalKey::Code(KeyCode::F10) => Key::F(10),
PhysicalKey::Code(KeyCode::F11) => Key::F(11),
PhysicalKey::Code(KeyCode::F12) => Key::F(12),
_ => Key::Unknown,
}
}