use std::sync::Arc;
pub use kui_core::widgets;
pub use kui_core::*;
mod access_bridge;
pub mod audio;
mod axis_lock;
mod backdrop;
mod clipboard;
mod dialogs;
mod ground;
mod icon;
mod keys;
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
mod linux_blur;
#[cfg(target_os = "macos")]
mod macos_chrome;
#[cfg(target_os = "macos")]
mod macos_drop;
#[cfg(target_os = "macos")]
mod macos_force;
#[cfg(target_os = "macos")]
mod macos_key;
#[cfg(target_os = "macos")]
mod macos_menu;
#[cfg(target_os = "macos")]
mod macos_open;
#[cfg(target_os = "macos")]
mod macos_text_input;
mod menus;
mod pacer;
mod pane;
mod popups;
mod retarget;
mod retry;
mod scroll_gesture;
mod secure_input;
pub mod testing;
mod windows;
use pane::{
Pane, appearance_of, apply_ime_off, ime_off_change, level_change, level_supported,
option_as_alt_change, sync_env, theme_appearance,
};
mod system_env;
mod system_fonts;
#[cfg(target_os = "windows")]
mod windows_anim;
#[cfg(target_os = "windows")]
mod windows_console;
#[cfg(target_os = "windows")]
mod windows_nc;
#[cfg(target_os = "windows")]
mod windows_theme;
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalPosition, LogicalSize};
use winit::event::{ElementState, Ime, MouseButton as WinitButton, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::keyboard::{Key as WinitKey, ModifiersState, NamedKey};
use winit::window::{CursorIcon, ResizeDirection, Window, WindowId as WinitWindowId};
pub trait App {
fn view(&mut self, ui: &mut Ui<'_>);
fn on_event(&mut self, _ev: UiEvent) {}
fn on_event_with(&mut self, ev: UiEvent, _core: &mut Core) {
self.on_event(ev)
}
fn setup(&mut self, _waker: Waker) {}
fn teardown(&mut self) {}
}
#[derive(Clone)]
pub struct Waker(EventLoopProxy<access_bridge::UserEvent>);
impl Waker {
pub fn wake(&self) {
let _ = self.0.send_event(access_bridge::UserEvent::Wake);
}
}
impl std::fmt::Debug for Waker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Waker")
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Chrome {
#[default]
Native,
Custom,
Borderless,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextAa {
#[default]
Auto,
Grayscale,
Subpixel,
}
pub fn app(title: &str) -> Launcher {
Launcher {
title: title.to_string(),
chrome: Chrome::Native,
size: (960.0, 640.0),
min_size: None,
max_size: None,
extensions: Extensions::new(),
text_aa: TextAa::Auto,
diagnostics: None,
core: None,
setup_core: Vec::new(),
deferred_events: false,
system: SystemEnv::default(),
icon: None,
icon_resource: None,
frame_latency: kui_wgpu::DEFAULT_FRAME_LATENCY,
backdrop: Backdrop::Opaque,
}
}
type CoreSetup = Box<dyn FnOnce(&mut Core)>;
pub struct Launcher {
title: String,
chrome: Chrome,
size: (f64, f64),
min_size: Option<(f64, f64)>,
max_size: Option<(f64, f64)>,
extensions: Extensions,
text_aa: TextAa,
core: Option<Core>,
setup_core: Vec<CoreSetup>,
diagnostics: Option<bool>,
deferred_events: bool,
system: SystemEnv,
icon: Option<winit::window::Icon>,
icon_resource: Option<u16>,
frame_latency: u32,
backdrop: Backdrop,
}
impl Launcher {
pub fn chrome(mut self, chrome: Chrome) -> Self {
self.chrome = chrome;
self
}
pub fn text_aa(mut self, aa: TextAa) -> Self {
self.text_aa = aa;
self
}
pub fn frame_latency(mut self, frames: u32) -> Self {
self.frame_latency = frames.max(1);
self
}
pub fn diagnostics(mut self, on: bool) -> Self {
self.diagnostics = Some(on);
self
}
pub fn devtools(self, on: bool) -> Self {
self.setup_core(move |core| core.set_devtools(on))
}
pub fn devtools_key(self, key: Accel) -> Self {
self.setup_core(move |core| core.set_devtools_key(key))
}
pub fn setup_core(mut self, f: impl FnOnce(&mut Core) + 'static) -> Self {
self.setup_core.push(Box::new(f));
self
}
pub fn core(mut self, core: Core) -> Self {
self.core = Some(core);
self
}
pub fn deferred_events(mut self) -> Self {
self.deferred_events = true;
self
}
pub fn system(mut self, pinned: SystemEnv) -> Self {
self.system = pinned;
self
}
pub fn icon(self, rgba: Vec<u8>, width: u32, height: u32) -> Self {
match self.try_icon(rgba, width, height) {
Ok(this) => this,
Err(e) => panic!("kui: {e}"),
}
}
pub fn try_icon(mut self, rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, String> {
self.icon = Some(icon::from_rgba(rgba, width, height)?);
Ok(self)
}
pub fn icon_resource(mut self, id: u16) -> Self {
self.icon_resource = Some(id);
self
}
pub fn backdrop(mut self, backdrop: Backdrop) -> Self {
self.backdrop = backdrop;
self
}
pub fn custom_titlebar(self) -> Self {
self.chrome(Chrome::Custom)
}
pub fn borderless(self) -> Self {
self.chrome(Chrome::Borderless)
}
pub fn size(mut self, w: f64, h: f64) -> Self {
self.size = (w, h);
self
}
pub fn min_size(mut self, w: f64, h: f64) -> Self {
self.min_size = Some((w, h));
self
}
pub fn max_size(mut self, w: f64, h: f64) -> Self {
self.max_size = Some((w, h));
self
}
pub fn extension(self, ext: impl Extension + 'static) -> Self {
let ns = ext.name().to_owned();
self.extension_as(ns, ext)
}
pub fn extension_as(self, namespace: impl Into<String>, ext: impl Extension + 'static) -> Self {
match self.try_extension_as(namespace, ext) {
Ok(this) => this,
Err(e) => panic!("kui: {e}"),
}
}
pub fn try_extension_as(
mut self,
namespace: impl Into<String>,
ext: impl Extension + 'static,
) -> Result<Self, String> {
self.extensions.push_as(namespace, Box::new(ext))?;
Ok(self)
}
pub fn with_extensions(mut self, extensions: Extensions) -> Self {
self.extensions = extensions;
self
}
pub fn extensions(mut self, exts: Vec<Box<dyn Extension>>) -> Self {
for ext in exts {
if let Err(e) = self.extensions.push(ext) {
panic!("kui: {e}");
}
}
self
}
fn shell<A: App>(mut self, app: A) -> Box<Shell<A>> {
let (diagnostics, session, core) = self.main_core();
Box::new(Shell {
title: self.title,
icon: icon::AppIcon::new(self.icon, self.icon_resource),
chrome: self.chrome,
size: clamp_size(self.size, self.min_size, self.max_size),
min_size: self.min_size,
max_size: self.max_size,
text_aa: self.text_aa,
frame_latency: wanted_frame_latency(self.frame_latency),
backdrop: self.backdrop,
diagnostics,
subpixel: false,
extensions: self.extensions,
session,
main_core: Some(core),
panes: Vec::new(),
gpu: None,
reopened: None,
reopen_owed: false,
pretended_loss: false,
locks: kui_core::KeyLocks::default(),
script: kui_core::LayoutScript::default(),
epoch: std::time::Instant::now(),
system: system_env::query(),
pinned_system: self.system,
clipboard: arboard::Clipboard::new().ok(),
#[cfg(target_os = "macos")]
native_menu: macos_menu::MacMenu::new(),
#[cfg(target_os = "macos")]
menu_shown: false,
#[cfg(target_os = "macos")]
native_menu_bar: macos_menu::MacMenuBar::new(),
#[cfg(target_os = "macos")]
applied_menu_bar: None,
audio: audio::Audio::new(),
audio_touch: std::time::Instant::now(),
smoke_frames: Self::smoke_frames(),
frames_drawn: 0,
exit_requested: false,
startup_error: None,
torn_down: false,
secure_input: secure_input::SecureInput::default(),
pumped: false,
opened: false,
primary_down: None,
armed: Vec::new(),
swallowed_press: None,
proxy: None,
next_deadline: None,
saw_event: false,
woke: false,
deferred_events: self.deferred_events,
owed: std::cell::Cell::new(false),
app,
})
}
#[cfg(test)]
fn dyn_shell<A: App + 'static>(self, app: A) -> Box<DynShell<'static>> {
self.shell(app)
}
fn main_core(&mut self) -> (bool, Session, Core) {
#[cfg(target_os = "windows")]
windows_console::attach_parent();
let diagnostics = self.diagnostics.unwrap_or(cfg!(debug_assertions));
let (session, mut core) = match self.core.take() {
Some(core) => (core.session().clone(), core),
None => {
let session = Session::new();
let core = Core::new_in(&session);
(session, core)
}
};
core.set_diagnostics(diagnostics);
core.devtools_from_env();
for f in std::mem::take(&mut self.setup_core) {
f(&mut core);
}
(diagnostics, session, core)
}
fn smoke_frames() -> Option<u32> {
if !(cfg!(debug_assertions) || cfg!(feature = "smoke")) {
return None;
}
std::env::var("KUI_SMOKE_FRAMES")
.ok()
.and_then(|s| s.parse().ok())
}
pub fn run<A: App>(self, app: A) -> Result<(), Box<dyn std::error::Error>> {
let event_loop = run_loop()?;
run_shell(event_loop, self.shell(app))
}
pub fn open<A: App>(self, app: A) -> Result<PumpRunner<A>, Box<dyn std::error::Error>> {
let event_loop = take_event_loop()?;
let mut shell = self.shell(app);
let state = PumpState::open(event_loop, &mut *shell);
let mut runner = PumpRunner {
state,
shell: std::mem::ManuallyDrop::new(shell),
};
if !runner.state.alive {
runner.retire();
}
if let Some(why) = runner.shell.startup_error.take() {
return Err(why.into());
}
Ok(runner)
}
}
fn run_loop() -> Result<EventLoop<access_bridge::UserEvent>, Box<dyn std::error::Error>> {
if PARKED_LOOP.with(|p| p.borrow().is_some()) {
return Err(
"kui: `run` cannot follow a pumped runner in this process — a loop \
a `PumpRunner` parked is still running; open another `PumpRunner`"
.into(),
);
}
Ok(EventLoop::<access_bridge::UserEvent>::with_user_event().build()?)
}
fn run_shell(
event_loop: EventLoop<access_bridge::UserEvent>,
mut shell: Box<DynShell<'_>>,
) -> Result<(), Box<dyn std::error::Error>> {
event_loop.set_control_flow(ControlFlow::Wait);
shell.attach(&event_loop);
event_loop.run_app(&mut Handler(&mut shell))?;
match shell.startup_error.take() {
Some(why) => Err(why.into()),
None => Ok(()),
}
}
impl DynShell<'_> {
fn fail_open(&mut self, event_loop: &ActiveEventLoop, why: String) {
self.startup_error = Some(why);
self.exit_requested = true;
if !self.pumped {
event_loop.exit();
}
}
fn attach(&mut self, event_loop: &EventLoop<access_bridge::UserEvent>) {
self.proxy = Some(event_loop.create_proxy());
self.app.setup(Waker(event_loop.create_proxy()));
#[cfg(target_os = "macos")]
if let Some(bar) = &self.native_menu_bar {
bar.set_waker(Waker(event_loop.create_proxy()));
}
#[cfg(target_os = "macos")]
macos_text_input::set_waker(Waker(event_loop.create_proxy()));
#[cfg(target_os = "macos")]
macos_drop::set_waker(Waker(event_loop.create_proxy()));
#[cfg(target_os = "macos")]
{
macos_open::set_waker(Waker(event_loop.create_proxy()));
macos_open::install();
}
system_fonts::watch(event_loop.create_proxy());
}
}
thread_local! {
static PARKED_LOOP: std::cell::RefCell<Option<EventLoop<access_bridge::UserEvent>>> =
const { std::cell::RefCell::new(None) };
}
fn take_event_loop() -> Result<EventLoop<access_bridge::UserEvent>, winit::error::EventLoopError> {
if let Some(parked) = PARKED_LOOP.with(|p| p.borrow_mut().take()) {
return Ok(parked);
}
EventLoop::<access_bridge::UserEvent>::with_user_event().build()
}
fn set_clipboard(clipboard: Option<&mut arboard::Clipboard>, text: String, html: Option<String>) {
let Some(cb) = clipboard else { return };
match html {
Some(html) => {
let _ = cb.set_html(html, Some(text));
}
None => {
let _ = cb.set_text(text);
}
}
}
fn input_completes(ev: &InputEvent) -> bool {
match ev {
InputEvent::MouseDown { .. }
| InputEvent::MouseUp { .. }
| InputEvent::ForceClick(_)
| InputEvent::Key(..)
| InputEvent::KeyDown(_)
| InputEvent::KeyUp(_)
| InputEvent::Text(_)
| InputEvent::Commit(_)
| InputEvent::Paste { .. }
| InputEvent::Access(_)
| InputEvent::DropFiles { .. }
| InputEvent::DragCancel
| InputEvent::Files(_)
| InputEvent::Open(_) => true,
InputEvent::CursorMoved(_)
| InputEvent::CursorLeft
| InputEvent::Scroll(_)
| InputEvent::ScrollGesture { .. }
| InputEvent::Preedit(..)
| InputEvent::Modifiers(_)
| InputEvent::DragFiles { .. } => false,
}
}
fn frame_waits_for_host(owed: bool, deferred_last: bool) -> bool {
owed && !deferred_last
}
fn pump_once(
event_loop: &mut EventLoop<access_bridge::UserEvent>,
shell: &mut DynShell<'_>,
) -> bool {
use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
match event_loop.pump_app_events(Some(std::time::Duration::ZERO), &mut Handler(shell)) {
PumpStatus::Continue => !shell.exit_requested,
PumpStatus::Exit(_) => false,
}
}
pub struct PumpRunner<A: App> {
state: PumpState,
shell: std::mem::ManuallyDrop<Box<Shell<A>>>,
}
struct PumpState {
event_loop: Option<EventLoop<access_bridge::UserEvent>>,
alive: bool,
pumps: u64,
woken_pumps: u64,
}
impl PumpState {
fn open(
mut event_loop: EventLoop<access_bridge::UserEvent>,
shell: &mut DynShell<'_>,
) -> PumpState {
event_loop.set_control_flow(ControlFlow::Wait);
shell.pumped = true;
shell.attach(&event_loop);
let alive = pump_once(&mut event_loop, shell);
let mut state = PumpState {
event_loop: Some(event_loop),
alive,
pumps: 1,
woken_pumps: 0,
};
state.tally(shell);
state
}
fn tally(&mut self, shell: &mut DynShell<'_>) {
if std::mem::take(&mut shell.woke) {
self.woken_pumps += 1;
}
}
fn pump(&mut self, shell: &mut DynShell<'_>) -> bool {
let Some(event_loop) = &mut self.event_loop else {
return false;
};
self.pumps += 1;
self.alive = pump_once(event_loop, shell);
self.tally(shell);
if !self.alive {
self.retire(shell);
}
self.alive
}
fn retire(&mut self, shell: &mut DynShell<'_>) {
self.alive = false;
shell.teardown_once();
let mut panes = std::mem::take(&mut shell.panes);
#[cfg(target_os = "macos")]
for pane in &panes {
macos_text_input::detach(&pane.window);
macos_drop::detach(&pane.window);
}
if !panes.is_empty() {
shell.main_core = Some(panes.remove(0).core);
}
if let Some(event_loop) = self.event_loop.take() {
PARKED_LOOP.with(|p| *p.borrow_mut() = Some(event_loop));
}
}
fn pump_until(&mut self, shell: &mut DynShell<'_>, deadline: std::time::Instant) -> bool {
use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus};
let Some(event_loop) = &mut self.event_loop else {
return false;
};
let timeout = deadline.saturating_duration_since(std::time::Instant::now());
self.pumps += 1;
self.alive = match event_loop.pump_app_events(Some(timeout), &mut Handler(shell)) {
PumpStatus::Continue => !shell.exit_requested,
PumpStatus::Exit(_) => false,
};
self.tally(shell);
if !self.alive {
self.retire(shell);
}
self.alive
}
fn waker(&self, shell: &DynShell<'_>) -> Waker {
match &self.event_loop {
Some(event_loop) => Waker(event_loop.create_proxy()),
None => Waker(shell.proxy.clone().expect("a pump runner keeps its proxy")),
}
}
}
impl DynShell<'_> {
fn core_mut_of(&mut self, id: WindowId) -> Option<&mut Core> {
if id == WindowId::MAIN {
return Some(self.core_mut());
}
self.panes
.iter_mut()
.find(|p| p.id == id)
.map(|p| &mut p.core)
}
fn window_id(&mut self, name: &str) -> Option<WindowId> {
self.core_mut()
.windows()
.into_iter()
.find(|(_, n)| &**n == name)
.map(|(id, _)| id)
}
fn request_redraw(&self) {
self.owed.set(false);
for p in &self.panes {
p.redraw_for(FrameCause::HOST);
}
}
}
impl<A: App> Drop for PumpRunner<A> {
fn drop(&mut self) {
self.retire();
let shell: Box<Shell<A>> = unsafe { std::mem::ManuallyDrop::take(&mut self.shell) };
drop_shell(shell);
}
}
fn drop_shell(shell: Box<DynShell<'_>>) {
drop(shell);
}
impl<A: App> PumpRunner<A> {
fn shell(&self) -> &DynShell<'_> {
&**self.shell
}
fn shell_mut(&mut self) -> &mut DynShell<'_> {
&mut **self.shell
}
fn retire(&mut self) {
self.state.retire(&mut **self.shell);
}
pub fn pump(&mut self) -> bool {
self.state.pump(&mut **self.shell)
}
pub fn pumps(&self) -> u64 {
self.state.pumps
}
pub fn woken_pumps(&self) -> u64 {
self.state.woken_pumps
}
pub fn pump_until(&mut self, deadline: std::time::Instant) -> bool {
self.state.pump_until(&mut **self.shell, deadline)
}
pub fn next_deadline(&self) -> Option<std::time::Instant> {
self.shell.next_deadline
}
pub fn waker(&self) -> Waker {
self.state.waker(&**self.shell)
}
pub fn app_mut(&mut self) -> &mut A {
&mut self.shell.app
}
pub fn route_events(
&mut self,
events: impl IntoIterator<Item = UiEvent>,
mut to_app: impl FnMut(&mut A, UiEvent, &mut Core),
) {
let Shell {
extensions,
app,
panes,
main_core,
..
} = &mut **self.shell;
extensions.route(events, |ev| {
if let Some(core) = window_core(panes, main_core, ev.window) {
to_app(app, ev, core);
}
});
}
pub fn core_mut(&mut self) -> &mut Core {
self.shell_mut().core_mut()
}
pub fn core_mut_of(&mut self, id: WindowId) -> Option<&mut Core> {
self.shell_mut().core_mut_of(id)
}
pub fn window_id(&mut self, name: &str) -> Option<WindowId> {
self.shell_mut().window_id(name)
}
pub fn window_size(&self) -> (Size, f32) {
self.shell().window_size()
}
pub fn request_redraw(&self) {
self.shell().request_redraw();
}
pub fn request_exit(&mut self) {
self.shell.exit_requested = true;
}
pub fn flush_audio(&mut self) {
self.shell_mut().apply_audio();
}
}
pub fn run<A: App>(
title: &str,
application: A,
extensions: Vec<Box<dyn Extension>>,
) -> Result<(), Box<dyn std::error::Error>> {
app(title).extensions(extensions).run(application)
}
fn clamp_size(size: (f64, f64), min: Option<(f64, f64)>, max: Option<(f64, f64)>) -> (f64, f64) {
let (mut w, mut h) = size;
if let Some((mw, mh)) = max {
w = w.min(mw);
h = h.min(mh);
}
if let Some((mw, mh)) = min {
w = w.max(mw);
h = h.max(mh);
}
(w, h)
}
const RESIZE_BAND: f32 = 6.0;
const DOUBLE_CLICK_MS: u128 = 350;
const MULTI_CLICK_MS: u128 = 400;
const MULTI_CLICK_SLOP: f32 = 4.0;
const BLINK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
const AUDIO_POLL: std::time::Duration = std::time::Duration::from_millis(50);
const AUDIO_IDLE_CLOSE: std::time::Duration = std::time::Duration::from_secs(5);
const REOPEN_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
const SURFACE_TRIES: u8 = 3;
fn reopen_due(last_try: Option<std::time::Instant>, now: std::time::Instant) -> std::time::Instant {
last_try.map_or(now, |t| (t + REOPEN_INTERVAL).max(now))
}
#[derive(Debug, PartialEq, Eq)]
enum Refused {
Retry,
GiveUp { say: bool },
}
fn surface_refused(tries: &mut u8) -> Refused {
*tries = tries.saturating_add(1);
if *tries <= SURFACE_TRIES {
Refused::Retry
} else {
Refused::GiveUp {
say: *tries == SURFACE_TRIES + 1,
}
}
}
fn subpixel_on(wanted: TextAa, dual_source: bool) -> bool {
match wanted {
TextAa::Grayscale => false,
TextAa::Subpixel | TextAa::Auto => dual_source,
}
}
fn aa_under(wanted: TextAa, backdrop: Backdrop) -> TextAa {
match wanted {
TextAa::Auto if backdrop.is_translucent() => TextAa::Grayscale,
other => other,
}
}
fn wanted_frame_latency(launcher: u32) -> u32 {
std::env::var("KUI_FRAME_LATENCY")
.ok()
.and_then(|v| v.trim().parse::<u32>().ok())
.filter(|n| *n >= 1)
.unwrap_or(launcher)
}
fn wanted_text_aa(launcher: TextAa) -> TextAa {
match std::env::var("KUI_TEXT_AA").ok().as_deref() {
Some("gray") | Some("grayscale") => TextAa::Grayscale,
Some("subpixel") | Some("lcd") => TextAa::Subpixel,
_ => launcher,
}
}
fn undecorated(attrs: winit::window::WindowAttributes) -> winit::window::WindowAttributes {
#[cfg(target_os = "macos")]
{
use winit::platform::macos::WindowAttributesExtMacOS;
attrs
.with_titlebar_transparent(true)
.with_fullsize_content_view(true)
.with_title_hidden(true)
.with_titlebar_buttons_hidden(true)
}
#[cfg(not(target_os = "macos"))]
{
attrs.with_decorations(false)
}
}
struct Armed {
id: WindowId,
inside: bool,
}
struct Shell<A: App + ?Sized> {
title: String,
icon: icon::AppIcon,
chrome: Chrome,
size: (f64, f64),
min_size: Option<(f64, f64)>,
max_size: Option<(f64, f64)>,
text_aa: TextAa,
frame_latency: u32,
backdrop: Backdrop,
diagnostics: bool,
subpixel: bool,
extensions: Extensions,
session: Session,
main_core: Option<Core>,
panes: Vec<Pane>,
gpu: Option<kui_wgpu::Gpu>,
reopened: Option<std::time::Instant>,
reopen_owed: bool,
pretended_loss: bool,
locks: kui_core::KeyLocks,
script: kui_core::LayoutScript,
epoch: std::time::Instant,
system: system_env::Queried,
pinned_system: SystemEnv,
clipboard: Option<arboard::Clipboard>,
#[cfg(target_os = "macos")]
native_menu: Option<macos_menu::MacMenu>,
#[cfg(target_os = "macos")]
menu_shown: bool,
#[cfg(target_os = "macos")]
native_menu_bar: Option<macos_menu::MacMenuBar>,
#[cfg(target_os = "macos")]
applied_menu_bar: Option<AppliedBar>,
audio: audio::Audio,
audio_touch: std::time::Instant,
smoke_frames: Option<u32>,
frames_drawn: u32,
exit_requested: bool,
startup_error: Option<String>,
torn_down: bool,
secure_input: secure_input::SecureInput,
pumped: bool,
opened: bool,
primary_down: Option<WindowId>,
armed: Vec<Armed>,
swallowed_press: Option<WindowId>,
proxy: Option<EventLoopProxy<access_bridge::UserEvent>>,
next_deadline: Option<std::time::Instant>,
saw_event: bool,
woke: bool,
deferred_events: bool,
owed: std::cell::Cell<bool>,
app: A,
}
type DynShell<'a> = Shell<dyn App + 'a>;
fn window_core<'a>(
panes: &'a mut [Pane],
main_core: &'a mut Option<Core>,
id: WindowId,
) -> Option<&'a mut Core> {
let at = panes
.iter()
.position(|p| p.id == id)
.or_else(|| panes.iter().position(|p| p.id == WindowId::MAIN));
match at {
Some(i) => Some(&mut panes[i].core),
None => main_core.as_mut(),
}
}
struct Handler<'s, 'a>(&'s mut DynShell<'a>);
impl ApplicationHandler<access_bridge::UserEvent> for Handler<'_, '_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.0.resumed(event_loop);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window: WinitWindowId,
event: WindowEvent,
) {
self.0.window_event(event_loop, window, event);
}
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
self.0.exiting(event_loop);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: access_bridge::UserEvent) {
self.0.user_event(event_loop, event);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.0.about_to_wait(event_loop);
}
}
impl DynShell<'_> {
fn core_mut(&mut self) -> &mut Core {
match self.panes.first_mut() {
Some(p) => &mut p.core,
None => self
.main_core
.as_mut()
.expect("the main core exists until its pane takes it"),
}
}
fn gpu_options(&self) -> kui_wgpu::GpuOptions {
kui_wgpu::GpuOptions::transparent(self.backdrop.is_translucent())
}
fn window_size(&self) -> (Size, f32) {
match self.panes.first() {
Some(p) => p.size(),
None => (Size::new(self.size.0 as f32, self.size.1 as f32), 1.0),
}
}
fn teardown_once(&mut self) {
self.secure_input.set(false);
if !self.torn_down {
self.torn_down = true;
self.app.teardown();
}
}
fn exit_main(&mut self, event_loop: &ActiveEventLoop) {
self.exit_requested = true;
if !self.pumped {
event_loop.exit();
}
}
fn file_drag_is_overridden(&self) -> bool {
#[cfg(target_os = "macos")]
{
macos_drop::installed()
}
#[cfg(not(target_os = "macos"))]
{
false
}
}
fn pump_file_drag(&mut self, event_loop: &ActiveEventLoop) {
#[cfg(target_os = "macos")]
for (delegate, msg) in macos_drop::take_messages() {
let Some(i) = self
.panes
.iter()
.position(|p| macos_drop::delegate_ptr(&p.window) == Some(delegate))
else {
continue;
};
let ev = match msg {
macos_drop::DragMsg::Over(paths, at) => InputEvent::DragFiles { paths, at },
macos_drop::DragMsg::Drop(paths, at) => InputEvent::DropFiles { paths, at },
macos_drop::DragMsg::Cancel => InputEvent::DragCancel,
};
if let Some(i) = self.dispatch(event_loop, i, ev) {
let pane = &self.panes[i];
macos_drop::stamp(&pane.window, pane.core.drop_target().is_some());
}
}
let batches: Vec<(WindowId, InputEvent)> = self
.panes
.iter_mut()
.filter_map(|pane| {
let dropped = pane.file_drag_pending.take()?;
let paths = std::mem::take(&mut pane.file_drag);
let at = pane.cursor;
let ev = if dropped {
InputEvent::DropFiles { paths, at }
} else {
InputEvent::DragFiles { paths, at }
};
Some((pane.id, ev))
})
.collect();
for (id, ev) in batches {
if let Some(i) = self.pane_of(id) {
self.dispatch(event_loop, i, ev);
}
}
}
fn pump_open_documents(&mut self, event_loop: &ActiveEventLoop) {
#[cfg(target_os = "macos")]
{
if !macos_open::pending() || self.pane_of(WindowId::MAIN).is_none() {
return;
}
for paths in macos_open::take() {
let Some(i) = self.pane_of(WindowId::MAIN) else {
macos_open::requeue(paths);
continue;
};
self.dispatch(event_loop, i, InputEvent::Open(paths));
}
}
#[cfg(not(target_os = "macos"))]
let _ = event_loop;
}
fn dispatch(
&mut self,
event_loop: &ActiveEventLoop,
i: usize,
ev: InputEvent,
) -> Option<usize> {
let t0 = std::time::Instant::now();
self.audio_touch = t0;
let completes = input_completes(&ev);
let here = self.panes[i].id;
let events = self.panes[i].core.handle_input(ev);
let reached_app = self.route_events(events);
self.owe_for(reached_app && completes);
self.apply_menu_actions(event_loop, i);
self.apply_window_commands(event_loop);
self.apply_audio();
self.pump_native_menu(event_loop);
let i = self.pane_of(here)?;
let pane = &mut self.panes[i];
pane.apply_cursor();
pane.core.stats.pending_input_ms += t0.elapsed().as_secs_f32() * 1e3;
pane.window.request_redraw();
Some(i)
}
fn apply_audio(&mut self) {
let core = self.core_mut();
let cmds = core.take_audio_commands();
let resources = core.resources.clone();
if !cmds.is_empty() {
self.audio_touch = std::time::Instant::now();
}
if resources.has_sounds() && self.audio_touch.elapsed() < AUDIO_IDLE_CLOSE {
self.audio.warm();
}
let answered = self.audio.apply(cmds, &resources);
if answered.truncated.is_empty() && answered.refused.is_empty() {
return;
}
let core = self.core_mut();
for (playback, at) in answered.truncated {
core.audio_truncated(playback, at);
}
for playback in answered.refused {
core.audio_refused(playback);
}
self.route_playback_events();
}
fn poll_audio(&mut self) {
let ended = self.audio.poll_ended();
if ended.is_empty() {
return;
}
let core = self.core_mut();
for playback in ended {
core.audio_ended(playback);
}
self.route_playback_events();
}
fn route_playback_events(&mut self) {
let pending = self.core_mut().take_pending_events();
if pending.is_empty() {
return;
}
self.route_events(pending);
for p in &self.panes {
p.redraw_for(FrameCause::AUDIO);
}
}
fn owe_for(&self, reached_app: bool) {
if reached_app && self.deferred_events {
self.owed.set(true);
}
}
fn route_events(&mut self, events: Vec<UiEvent>) -> bool {
let mut reached_app = false;
let Shell {
extensions,
app,
panes,
main_core,
..
} = self;
extensions.route(events, |ev| {
reached_app = true;
match window_core(panes, main_core, ev.window) {
Some(core) => app.on_event_with(ev, core),
None => app.on_event(ev),
}
});
if reached_app && self.panes.len() > 1 {
for p in &self.panes {
p.redraw_for(FrameCause::ELSEWHERE);
}
}
reached_app
}
fn after_direct_edit(&mut self, i: usize) {
let events = self.panes[i].core.take_pending_events();
let reached_app = self.route_events(events);
self.owe_for(reached_app);
self.panes[i].redraw_for(FrameCause::KEY);
}
fn redraw(&mut self, i: usize) {
let Shell {
app,
extensions,
panes,
min_size,
max_size,
epoch,
system,
pinned_system,
audio,
pretended_loss,
..
} = self;
let pane = &mut panes[i];
if let Some(g) = pane.ground.as_mut()
&& let Err(why) = g.settle()
{
eprintln!("kui: no wallpaper to draw behind the window: {why}");
pane.ground = None;
pane.backdrop = Backdrop::Opaque;
}
sync_env(pane, system, *pinned_system, audio.env());
let window = &pane.window;
let (viewport, scale) = pane.size();
let size = window.inner_size();
let t_view = std::time::Instant::now();
pane.core.set_time(epoch.elapsed().as_secs_f64());
pane.core.note_frame_cause(pane.cause.take());
let mut ui = pane.core.frame_with(viewport, scale, extensions);
app.view(&mut ui);
let view_ms = t_view.elapsed().as_secs_f32() * 1e3;
let t_layout = std::time::Instant::now();
ui.finish();
let layout_ms = t_layout.elapsed().as_secs_f32() * 1e3;
for w in pane.core.take_warnings() {
eprintln!(
"kui: warning [{}] node {:016x}: {}",
w.code, w.key.0, w.message
);
}
#[cfg(target_os = "windows")]
if let Some(nc) = &pane.nc {
nc.update(
scale,
window.is_maximized(),
pane.core
.interaction
.hits()
.iter()
.map(|h| (h.window, h.rect.intersect(&h.clip))),
);
}
if let Some(t) = pane.core.window_title()
&& t != pane.applied_title
{
pane.applied_title = t.to_string();
window.set_title(&pane.applied_title);
}
if let Some(level) = level_change(
pane.kind,
pane.core.always_on_top(),
&mut pane.applied_on_top,
) {
window.set_window_level(level);
}
if let Some(_option_as_alt) =
option_as_alt_change(pane.core.option_as_alt(), &mut pane.applied_option_as_alt)
{
#[cfg(target_os = "macos")]
{
use winit::platform::macos::{OptionAsAlt as Winit, WindowExtMacOS};
window.set_option_as_alt(match _option_as_alt {
kui_core::OptionAsAlt::None => Winit::None,
kui_core::OptionAsAlt::Left => Winit::OnlyLeft,
kui_core::OptionAsAlt::Right => Winit::OnlyRight,
kui_core::OptionAsAlt::Both => Winit::Both,
});
}
}
if let Some(off) = ime_off_change(pane.ime_off_wanted(), &mut pane.applied_ime_off) {
apply_ime_off(window, off);
}
if pane.id == WindowId::MAIN
&& let Some((mw, mh)) = *min_size
{
let inset = pane.core.devtools_inset();
let want = clamp_size((mw + inset.w as f64, mh + inset.h as f64), None, *max_size);
if pane.applied_min != Some(want) {
pane.applied_min = Some(want);
window.set_min_inner_size(Some(LogicalSize::new(want.0, want.1)));
}
}
if let Some(r) = pane.core.ime_rect() {
window.set_ime_cursor_area(
winit::dpi::LogicalPosition::new(r.x, r.y),
winit::dpi::LogicalSize::new(r.w.max(1.0), r.h),
);
}
#[cfg(target_os = "macos")]
macos_text_input::stamp(window, &pane.core);
let t_render = std::time::Instant::now();
if let Some(secs) = lose_device_at()
&& !*pretended_loss
&& epoch.elapsed().as_secs_f64() >= secs
&& let Some(r) = pane.renderer.as_ref()
{
*pretended_loss = true;
r.gpu().mark_lost();
}
let ground = pane.core.theme().bg;
if let (Some(g), Some(r)) = (pane.ground.as_mut(), pane.renderer.as_mut()) {
g.prepare(window, r);
}
let clear = if pane.backdrop.is_translucent() && pane.ground.is_none() {
kui_wgpu::wgpu::Color::TRANSPARENT
} else {
kui_wgpu::wgpu::Color {
r: ground.r as f64,
g: ground.g as f64,
b: ground.b as f64,
a: ground.a as f64,
}
};
let (dl, atlas) = pane.core.output();
let mut wait_ms = 0.0;
let mut reopen = false;
let drawn = pane.renderer.as_mut().map(|r| {
r.clear_color = clear;
r.render(dl, atlas)
});
match drawn {
Some(Ok(report)) => {
wait_ms = report.vsync_wait_ms;
pane.pacer
.presented(std::time::Instant::now(), (size.width, size.height));
pane.retry.presented();
pane.surface_tries = 0;
pane.awaits_device = false;
}
Some(Err(kui_wgpu::RenderError::Reconfigure)) => {
if let Some(r) = pane.renderer.as_mut() {
r.resize(size.width, size.height);
}
pane.redraw_for(FrameCause::RETRY);
}
Some(Err(kui_wgpu::RenderError::Validation)) => {
match surface_refused(&mut pane.surface_tries) {
Refused::Retry => {
if let Some(r) = pane.renderer.as_mut() {
r.resize(size.width, size.height);
}
pane.redraw_for(FrameCause::RETRY);
}
Refused::GiveUp { say } => {
if say {
eprintln!("kui: the surface stays invalid; reopening the device");
}
reopen = true;
}
}
}
Some(Err(kui_wgpu::RenderError::Skip)) => {
let now = std::time::Instant::now();
pane.retry.skipped(now);
pane.pacer.presented(now, (size.width, size.height));
}
Some(Err(kui_wgpu::RenderError::DeviceLost)) | None => reopen = true,
}
pane.awaits_device |= reopen;
let render_ms = (t_render.elapsed().as_secs_f32() * 1e3 - wait_ms).max(0.0);
pane.core.stats.push(FrameSample {
input_ms: 0.0,
view_ms,
layout_ms,
render_ms,
wait_ms,
});
if reopen {
self.reopen_device();
}
}
fn reopen_device(&mut self) {
let now = std::time::Instant::now();
if reopen_due(self.reopened, now) > now {
self.reopen_owed = true;
return;
}
self.reopened = Some(now);
for pane in &mut self.panes {
pane.renderer = None;
pane.surface_tries = 0;
}
self.gpu = None;
let mut gpu: Option<kui_wgpu::Gpu> = None;
let options = self.gpu_options();
for pane in &mut self.panes {
let px = pane.window.inner_size();
let made = match &gpu {
None => pollster::block_on(kui_wgpu::Renderer::new_with(
pane.window.clone(),
px.width,
px.height,
options,
)),
Some(gpu) => kui_wgpu::Renderer::new_in_with(
gpu,
pane.window.clone(),
px.width,
px.height,
pane.backdrop.is_translucent(),
),
};
match made {
Ok(mut r) => {
r.set_frame_latency(self.frame_latency);
if pane.backdrop.is_translucent() && pane.ground.is_none() && !r.transparent() {
pane.backdrop = Backdrop::Opaque;
}
if let Some(g) = pane.ground.as_mut() {
g.renderer_replaced();
}
let opened = gpu.is_none();
gpu.get_or_insert_with(|| r.gpu().clone());
if opened {
eprintln!("kui: device reopened");
}
pane.renderer = Some(r);
pane.redraw_for(FrameCause::DEVICE);
}
Err(err) => eprintln!("kui: cannot reopen window {}: {err}", pane.id.0),
}
pane.awaits_device = pane.renderer.is_none();
}
self.reopen_owed = self.panes.iter().any(|p| p.awaits_device);
if let Some(gpu) = &gpu {
let wanted = aa_under(wanted_text_aa(self.text_aa), self.backdrop);
let on = subpixel_on(wanted, gpu.dual_source());
if on != self.subpixel {
self.subpixel = on;
for pane in &mut self.panes {
pane.core.set_subpixel_text(on);
}
}
}
self.gpu = gpu;
}
}
#[cfg(target_os = "macos")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AppliedBar {
Standard,
Declared(WindowId, u64),
}
impl ApplicationHandler<access_bridge::UserEvent> for DynShell<'_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if !self.panes.is_empty() || self.opened {
return;
}
self.opened = true;
let size = std::env::var("KUI_WINDOW")
.ok()
.and_then(|s| {
let (w, h) = s.split_once('x')?;
Some((w.parse().ok()?, h.parse().ok()?))
})
.map(|s| clamp_size(s, self.min_size, self.max_size))
.unwrap_or(self.size);
let mut attrs = self.window_attrs(&self.title.clone(), size, self.chrome, self.backdrop);
if let Some((mw, mh)) = self.min_size {
attrs = attrs.with_min_inner_size(LogicalSize::new(mw, mh));
}
if let Some((mw, mh)) = self.max_size {
attrs = attrs.with_max_inner_size(LogicalSize::new(mw, mh));
}
if let Some((x, y)) = std::env::var("KUI_WINDOW_AT").ok().and_then(|s| {
let (x, y) = s.split_once(',')?;
Some((x.parse::<f64>().ok()?, y.parse::<f64>().ok()?))
}) {
attrs = attrs.with_position(LogicalPosition::new(x, y));
}
let window = match event_loop.create_window(attrs) {
Ok(w) => Arc::new(w),
Err(e) => return self.fail_open(event_loop, format!("cannot open the window: {e}")),
};
self.icon.set_on(&window);
let px = window.inner_size();
let renderer = pollster::block_on(kui_wgpu::Renderer::new_with(
window.clone(),
px.width,
px.height,
self.gpu_options(),
));
let mut renderer = match renderer {
Ok(r) => r,
Err(e) => return self.fail_open(event_loop, format!("cannot draw in the window: {e}")),
};
renderer.set_frame_latency(self.frame_latency);
let backdrop = backdrop::apply(&window, &renderer, self.backdrop);
self.subpixel = subpixel_on(
aa_under(wanted_text_aa(self.text_aa), self.backdrop),
renderer.subpixel_text(),
);
self.gpu = Some(renderer.gpu().clone());
let mut core = self
.main_core
.take()
.expect("the main core is built once, by the launcher");
core.set_subpixel_text(self.subpixel);
core.env.window.id = WindowId::MAIN;
self.push_pane(
event_loop,
WindowId::MAIN,
WindowConfig::default(),
WindowId::MAIN,
self.chrome,
core,
window,
renderer,
backdrop,
);
self.panes[0].applied_title = self.title.clone();
self.panes[0].applied_min = self.min_size;
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
id: WinitWindowId,
event: WindowEvent,
) {
let Some(i) = self.pane_index(id) else { return };
if !matches!(event, WindowEvent::RedrawRequested) {
self.saw_event = true;
}
{
let pane = &mut self.panes[i];
if let Some(bridge) = &mut pane.access {
bridge.process_event(&pane.window, &event);
}
}
match event {
WindowEvent::CloseRequested => {
if self.panes[i].id == WindowId::MAIN {
self.exit_main(event_loop);
} else {
let id = self.panes[i].id;
self.close_pane(event_loop, id);
}
}
WindowEvent::Resized(size) => {
let pane = &mut self.panes[i];
if let Some(r) = pane.renderer.as_mut() {
r.resize(size.width, size.height);
}
if let Some(g) = pane.ground.as_mut() {
g.moved();
}
if pane.minimized() {
pane.cause.went_dark();
pane.redraw_for(FrameCause::RESIZE);
} else if pane.cause.is_dark() && cfg!(target_os = "windows") {
pane.came_back(FrameCause::RESIZE);
} else {
pane.redraw_for(FrameCause::RESIZE);
}
}
WindowEvent::ScaleFactorChanged { .. } => {
self.panes[i].redraw_for(FrameCause::SCALE);
}
WindowEvent::Moved(_) => {
let pane = &mut self.panes[i];
if let Some(g) = pane.ground.as_mut() {
g.moved();
pane.redraw_for(FrameCause::RESIZE);
}
}
WindowEvent::CursorMoved { position, .. } => {
let pane = &mut self.panes[i];
let scale = pane.window.scale_factor() as f32;
let p = Vec2::new(position.x as f32 / scale, position.y as f32 / scale);
if pane.synthesizes_resize() {
pane.resize_edge = pane.resize_edge_at(p);
}
if pane.cursor != p {
pane.scroll_gesture.pointer_moved();
}
pane.cursor = p;
let from = pane.id;
self.dispatch(event_loop, i, InputEvent::CursorMoved(p));
self.retarget_move(event_loop, from, p);
}
WindowEvent::CursorLeft { .. } => {
self.dispatch(event_loop, i, InputEvent::CursorLeft);
}
WindowEvent::HoveredFile(path) => {
if !self.file_drag_is_overridden() {
let pane = &mut self.panes[i];
pane.file_drag.push(path.to_string_lossy().into_owned());
pane.file_drag_pending = Some(false);
}
}
WindowEvent::DroppedFile(path) => {
if !self.file_drag_is_overridden() {
let pane = &mut self.panes[i];
if pane.file_drag_pending != Some(true) {
pane.file_drag.clear();
}
pane.file_drag.push(path.to_string_lossy().into_owned());
pane.file_drag_pending = Some(true);
}
}
WindowEvent::HoveredFileCancelled => {
if !self.file_drag_is_overridden() {
let pane = &mut self.panes[i];
pane.file_drag.clear();
pane.file_drag_pending = None;
self.dispatch(event_loop, i, InputEvent::DragCancel);
}
}
WindowEvent::Focused(focused) => {
self.panes[i].os_focused = focused;
if !focused {
self.panes[i].modifier_keys_down.clear();
self.panes[i].ime_off_held.clear();
self.panes[i].sync_ime();
}
if focused {
let before = (self.system, self.panes[i].appearance);
self.system = system_env::query();
self.panes[i].appearance = appearance_of(&self.panes[i].window);
if before != (self.system, self.panes[i].appearance) {
for p in &self.panes {
p.redraw_for(FrameCause::APPEARANCE);
}
}
}
}
WindowEvent::Occluded(covered) => {
self.panes[i]
.retry
.occluded(covered, std::time::Instant::now());
if covered {
self.panes[i].cause.went_dark();
} else {
self.panes[i].came_back(FrameCause::OCCLUSION);
}
}
WindowEvent::ThemeChanged(theme) => {
self.panes[i].appearance = theme_appearance(Some(theme));
self.system = system_env::query();
self.panes[i].redraw_for(FrameCause::APPEARANCE);
}
WindowEvent::ModifiersChanged(m) => {
let t = self.key_target(i);
self.panes[i].modifiers = m.state();
self.panes[t].modifiers = m.state();
use winit::keyboard::ModifiersKeyState::Pressed;
let alt = (m.lalt_state() == Pressed, m.ralt_state() == Pressed);
self.panes[i].alt_held = alt;
self.panes[t].alt_held = alt;
let kmods = self.panes[t].kmods();
self.dispatch(event_loop, t, InputEvent::Modifiers(kmods));
}
WindowEvent::KeyboardInput {
event,
is_synthetic,
..
} => {
if !(is_synthetic && event.state == ElementState::Pressed) {
let t = self.key_target(i);
self.on_key(event_loop, i, t, event)
}
}
WindowEvent::Ime(Ime::Commit(text)) => {
self.panes[i].preedit_open = false;
let t = self.key_target(i);
self.dispatch(event_loop, t, InputEvent::Commit(text));
}
WindowEvent::Ime(Ime::Preedit(text, cursor)) => {
self.panes[i].preedit_open = !text.is_empty();
let t = self.key_target(i);
self.dispatch(event_loop, t, InputEvent::Preedit(text, cursor));
}
WindowEvent::Ime(Ime::Disabled) => {
if std::mem::take(&mut self.panes[i].preedit_open) {
let t = self.key_target(i);
self.dispatch(event_loop, t, InputEvent::Preedit(String::new(), None));
}
}
WindowEvent::TouchpadPressure { stage, .. } => {
let pane = &mut self.panes[i];
let was = std::mem::replace(&mut pane.pressure_stage, stage);
if stage >= 2 && was < 2 {
let at = pane.cursor;
self.dispatch(event_loop, i, InputEvent::ForceClick(at));
}
}
WindowEvent::MouseWheel { delta, phase, .. } => {
let started = phase == winit::event::TouchPhase::Started;
let pane = &mut self.panes[i];
let scale = pane.window.scale_factor() as f32;
let now = std::time::Instant::now();
let (d, kind) = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => {
pane.axis_lock.end();
(
Vec2::new(Core::lines_to_px(x), Core::lines_to_px(y)),
scroll_gesture::Kind::Line,
)
}
winit::event::MouseScrollDelta::PixelDelta(p) => {
if pane.scroll_gesture.phase(phase, now) {
pane.axis_lock.end();
}
(
pane.axis_lock
.pixel(Vec2::new(p.x as f32 / scale, p.y as f32 / scale), now),
scroll_gesture::Kind::Pixel,
)
}
};
if d != Vec2::ZERO {
let begins = pane.scroll_gesture.begins(kind, started, now);
self.dispatch(
event_loop,
i,
InputEvent::ScrollGesture { delta: d, begins },
);
} else {
pane.scroll_gesture.note(kind, started, now);
}
}
WindowEvent::MouseInput { state, button, .. } => {
let button = match button {
WinitButton::Left => MouseButton::Primary,
WinitButton::Right => MouseButton::Secondary,
WinitButton::Middle => MouseButton::Middle,
WinitButton::Back => MouseButton::Other(0),
WinitButton::Forward => MouseButton::Other(1),
WinitButton::Other(n) => {
MouseButton::Other(n.saturating_add(2).min(u8::MAX as u16) as u8)
}
};
let primary = button == MouseButton::Primary;
let here = self.panes[i].id;
if primary {
self.primary_down = (state == ElementState::Pressed).then_some(here);
if state == ElementState::Pressed {
self.swallowed_press = None;
}
}
if state == ElementState::Pressed {
let mut consumed = false;
let mut reached_app = false;
for (id, activates) in self.popups_outside(i) {
reached_app |= self.dismiss(id, DismissReason::Outside);
consumed |= primary && !activates;
}
if consumed {
self.swallowed_press = Some(here);
self.owe_for(reached_app);
for p in &self.panes {
p.redraw_for(FrameCause::BUTTON);
}
return;
}
}
if primary
&& state == ElementState::Pressed
&& self.panes[i].kind == WindowKind::Popup
&& !self.panes[i].activates
&& !self.armed.iter().any(|a| a.id == here)
{
self.armed.push(Armed {
id: here,
inside: true,
});
}
if state == ElementState::Released && primary {
if self.swallowed_press == Some(here) {
self.swallowed_press = None;
self.primary_down = None;
return;
}
let at = self.panes[i].cursor;
self.classify_release(event_loop, here, at);
}
let Some(i) = self.pane_of(here) else { return };
let pane = &mut self.panes[i];
if primary
&& state == ElementState::Pressed
&& let Some(dir) = pane.resize_edge
{
let _ = pane.window.drag_resize_window(dir);
return;
}
let ev = match state {
ElementState::Pressed => {
let clicks = if primary {
let now = std::time::Instant::now();
let clicks = match pane.last_click {
Some((t, p, n))
if now.duration_since(t).as_millis() < MULTI_CLICK_MS
&& (p.x - pane.cursor.x).abs() < MULTI_CLICK_SLOP
&& (p.y - pane.cursor.y).abs() < MULTI_CLICK_SLOP =>
{
n % 3 + 1
}
_ => 1,
};
pane.last_click = Some((now, pane.cursor, clicks));
clicks
} else {
1
};
InputEvent::MouseDown { button, clicks }
}
ElementState::Released => InputEvent::MouseUp { button },
};
self.dispatch(event_loop, i, ev);
}
WindowEvent::RedrawRequested
if frame_waits_for_host(self.owed.get(), self.panes[i].deferred_frame) =>
{
self.panes[i].deferred_frame = true;
}
WindowEvent::RedrawRequested if !self.panes[i].admit_frame() => {}
WindowEvent::RedrawRequested => {
self.panes[i].deferred_frame = false;
self.redraw(i);
if let Some(n) = self.smoke_frames
&& let Some(p) = self.panes.get(i)
&& p.id == WindowId::MAIN
&& p.retry.presented_once()
{
self.frames_drawn += 1;
if self.frames_drawn >= n {
self.exit_requested = true;
} else {
self.panes[i].redraw_for(FrameCause::SMOKE);
}
}
self.panes[i].publish_access();
self.apply_menu_actions(event_loop, i);
self.apply_window_commands(event_loop);
self.apply_audio();
let Some(i) = self.pane_index(id) else { return };
self.panes[i].apply_cursor();
let pending = self.panes[i].core.take_pending_events();
if !pending.is_empty() {
self.route_events(pending);
if let Some(p) = self.panes.get(i) {
p.redraw_for(FrameCause::AFTER_FRAME);
}
}
#[cfg(target_os = "windows")]
if let Some(p) = self.panes.get_mut(i) {
let animating = p.animates_now();
if let Some(t) = &mut p.anim_timer {
t.set(animating);
}
}
}
_ => {}
}
if self.exit_requested {
self.exit_main(event_loop);
}
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
self.teardown_once();
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: access_bridge::UserEvent) {
self.saw_event = true;
if matches!(event, access_bridge::UserEvent::Wake) {
for pane in &self.panes {
pane.redraw_for(FrameCause::WAKE);
}
return;
}
if matches!(event, access_bridge::UserEvent::FontsChanged) {
system_fonts::handled();
let changed = self
.panes
.first_mut()
.map_or(0, |pane| pane.core.reload_system_fonts());
if changed > 0 {
for pane in &self.panes {
pane.redraw_for(FrameCause::APPEARANCE);
}
}
return;
}
let Some(i) = access_bridge::window_of(&event).and_then(|w| self.pane_index(w)) else {
return;
};
let event = match event {
access_bridge::UserEvent::Files { paths, .. } => {
self.dispatch(event_loop, i, InputEvent::Files(paths));
return;
}
other => other,
};
let Some(bridge) = &mut self.panes[i].access else {
return;
};
let was_listening = bridge.active();
let req = bridge.on_event(event);
if bridge.active() != was_listening {
self.panes[i].redraw_for(FrameCause::APPEARANCE);
}
if let Some(req) = req {
self.dispatch(event_loop, i, InputEvent::Access(req));
}
if let Some(pane) = self.panes.get_mut(i) {
pane.publish_access();
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if self.pumped && !self.opened && !self.exit_requested {
self.resumed(event_loop);
}
self.pump_native_menu(event_loop);
self.pump_menu_bar(event_loop);
self.pump_text_input(event_loop);
self.pump_file_drag(event_loop);
self.pump_open_documents(event_loop);
self.settle_focus();
self.apply_secure_input();
self.dismiss_popups_if_deactivated();
self.poll_audio();
self.apply_audio();
let now = std::time::Instant::now();
let mut deadline: Option<std::time::Instant> = None;
if self.reopen_owed {
if reopen_due(self.reopened, now) <= now {
self.reopen_device();
}
if self.reopen_owed {
let at = reopen_due(self.reopened, now);
deadline = Some(deadline.map_or(at, |d| d.min(at)));
}
}
for pane in &mut self.panes {
if pane.animates_now() && !pane.pacer.holding() {
match pane.retry.animation_waits(now) {
None => pane.redraw_for(FrameCause::OWED),
Some(at) => deadline = Some(deadline.map_or(at, |d| d.min(at))),
}
}
if let Some((at, due)) = pane.pacer.overdue(now) {
if due {
pane.redraw_for(FrameCause::OVERDUE);
} else {
deadline = Some(deadline.map_or(at, |d| d.min(at)));
}
}
let (ask, wake) = pane.retry.poll(now);
if ask {
pane.redraw_for(FrameCause::RETRY);
}
if let Some(at) = wake {
deadline = Some(deadline.map_or(at, |d| d.min(at)));
}
if !pane.core.has_caret() {
if !pane.blink_visible {
pane.blink_visible = true;
pane.core.set_caret_visible(true);
pane.redraw_for(FrameCause::CARET);
}
pane.blink_deadline = None;
continue;
}
if !pane.core.env.focused {
if pane.blink_visible {
pane.blink_visible = false;
pane.core.set_caret_visible(false);
pane.redraw_for(FrameCause::CARET);
}
pane.blink_deadline = None;
continue;
}
let stamp = pane.core.caret_stamp();
if stamp != pane.caret_stamp_seen || pane.blink_deadline.is_none() {
pane.caret_stamp_seen = stamp;
pane.blink_deadline = Some(now + BLINK_INTERVAL);
if !pane.blink_visible {
pane.blink_visible = true;
pane.core.set_caret_visible(true);
pane.redraw_for(FrameCause::CARET);
}
} else if now >= pane.blink_deadline.unwrap() {
pane.blink_visible = !pane.blink_visible;
pane.core.set_caret_visible(pane.blink_visible);
pane.blink_deadline = Some(now + BLINK_INTERVAL);
pane.redraw_for(FrameCause::CARET);
}
if let Some(d) = pane.blink_deadline {
deadline = Some(deadline.map_or(d, |e| e.min(d)));
}
}
if self.audio.active() {
let poll = now + AUDIO_POLL;
deadline = Some(deadline.map_or(poll, |d| d.min(poll)));
}
if self.audio.holds_device() {
if !self.audio.active() && self.audio_touch.elapsed() >= AUDIO_IDLE_CLOSE {
self.audio.close();
}
if self.audio.holds_device() {
let at = (self.audio_touch + AUDIO_IDLE_CLOSE).max(now + AUDIO_POLL);
deadline = Some(deadline.map_or(at, |d| d.min(at)));
}
}
if std::mem::take(&mut self.saw_event) {
deadline = Some(now);
self.woke = true;
}
self.next_deadline = deadline;
event_loop.set_control_flow(match deadline {
Some(d) => ControlFlow::WaitUntil(d),
None => ControlFlow::Wait,
});
}
}
pub use kui_wgpu::{RenderError, Renderer, wgpu};
fn lose_device_at() -> Option<f64> {
static AT: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
*AT.get_or_init(|| std::env::var("KUI_LOSE_DEVICE").ok()?.parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
struct Empty;
impl App for Empty {
fn view(&mut self, _ui: &mut Ui<'_>) {}
}
#[test]
fn an_app_that_borrows_still_runs() {
struct Borrowing<'a>(&'a str);
impl App for Borrowing<'_> {
fn view(&mut self, ui: &mut Ui<'_>) {
ui.text(self.0, TextStyle::new(12.0));
}
}
let title = String::from("t");
let _run = || app("t").run(Borrowing(&title));
let _open = || {
let mut runner = app("t").open(Borrowing(&title))?;
let Borrowing(s) = runner.app_mut();
assert_eq!(*s, "t");
Ok::<_, Box<dyn std::error::Error>>(())
};
}
#[test]
fn teardown_runs_once_across_retire_and_drop() {
use std::cell::Cell;
use std::rc::Rc;
struct Counting(Rc<Cell<u32>>);
impl App for Counting {
fn view(&mut self, _ui: &mut Ui<'_>) {}
fn teardown(&mut self) {
self.0.set(self.0.get() + 1);
}
}
let count = Rc::new(Cell::new(0));
let runner_of = |app: Counting| PumpRunner {
state: PumpState {
event_loop: None,
alive: true,
pumps: 0,
woken_pumps: 0,
},
shell: std::mem::ManuallyDrop::new(super::app("t").diagnostics(false).shell(app)),
};
let mut runner = runner_of(Counting(count.clone()));
assert_eq!(count.get(), 0, "nothing before the end");
runner.request_exit();
assert_eq!(count.get(), 0, "asking is not the end");
runner.retire();
assert_eq!(count.get(), 1, "retiring is");
assert!(!runner.state.alive);
runner.retire();
runner.shell_mut().teardown_once();
assert_eq!(count.get(), 1, "once, however many ends come after");
drop(runner);
assert_eq!(
count.get(),
1,
"the drop retires again, and it is still once"
);
let count = Rc::new(Cell::new(0));
drop(runner_of(Counting(count.clone())));
assert_eq!(count.get(), 1);
}
#[test]
fn a_woken_turn_is_counted_once() {
let mut runner = PumpRunner {
state: PumpState {
event_loop: None,
alive: true,
pumps: 0,
woken_pumps: 0,
},
shell: std::mem::ManuallyDrop::new(super::app("t").diagnostics(false).shell(Empty)),
};
let tally = |r: &mut PumpRunner<Empty>| r.state.tally(&mut **r.shell);
tally(&mut runner);
assert_eq!(runner.woken_pumps(), 0, "a quiet turn is not woken");
runner.shell_mut().woke = true;
tally(&mut runner);
assert_eq!(runner.woken_pumps(), 1);
assert!(!runner.shell().woke, "taken by the turn that counted it");
tally(&mut runner);
tally(&mut runner);
assert_eq!(
runner.woken_pumps(),
1,
"and the quiet turns after it are quiet"
);
}
#[test]
fn only_a_discrete_input_is_worth_waiting_for() {
assert!(input_completes(&InputEvent::mouse_down(1)));
assert!(input_completes(&InputEvent::mouse_up()));
assert!(input_completes(&InputEvent::Text("x".into())));
assert!(!input_completes(&InputEvent::CursorMoved(Vec2::ZERO)));
assert!(!input_completes(&InputEvent::Scroll(Vec2::ZERO)));
assert!(!input_completes(&InputEvent::CursorLeft));
}
#[test]
fn a_frame_never_waits_twice_running() {
assert!(frame_waits_for_host(true, false));
assert!(!frame_waits_for_host(true, true));
assert!(!frame_waits_for_host(false, false));
assert!(!frame_waits_for_host(false, true));
}
#[test]
fn deferred_events_is_opt_in() {
assert!(!app("t").deferred_events);
assert!(app("t").deferred_events().deferred_events);
}
#[test]
fn clamp_size_honors_each_bound() {
let bounds = (Some((400.0, 300.0)), Some((1200.0, 900.0)));
assert_eq!(
clamp_size((800.0, 600.0), bounds.0, bounds.1),
(800.0, 600.0)
);
assert_eq!(
clamp_size((100.0, 100.0), bounds.0, bounds.1),
(400.0, 300.0)
);
assert_eq!(
clamp_size((4000.0, 4000.0), bounds.0, bounds.1),
(1200.0, 900.0)
);
assert_eq!(
clamp_size((100.0, 4000.0), bounds.0, bounds.1),
(400.0, 900.0)
);
assert_eq!(clamp_size((10.0, 10.0), None, None), (10.0, 10.0));
assert_eq!(
clamp_size((800.0, 600.0), Some((500.0, 500.0)), Some((200.0, 200.0))),
(500.0, 500.0)
);
}
#[test]
fn diagnostics_follow_the_build_unless_told_otherwise() {
assert_eq!(
app("t").dyn_shell(Empty).core_mut().diagnostics(),
cfg!(debug_assertions)
);
assert!(
app("t")
.diagnostics(true)
.dyn_shell(Empty)
.core_mut()
.diagnostics()
);
assert!(
!app("t")
.diagnostics(false)
.dyn_shell(Empty)
.core_mut()
.diagnostics()
);
}
#[test]
fn a_handed_core_is_the_main_window_s_and_its_session_the_app_s() {
let session = Session::new();
let mut core = Core::new_in(&session);
core.set_devtools(true);
core.set_native_menus(false);
let image = core.resources.add_image(1, 1, vec![0; 4]);
let mut shell = app("t").diagnostics(false).core(core).dyn_shell(Empty);
assert!(shell.session.is(&session), "the session came along");
assert!(shell.core_mut().devtools(), "the devtools door held");
assert!(!shell.core_mut().native_menus(), "so did the menus one");
assert!(
!shell.core_mut().diagnostics(),
"the launcher's own setting lands after"
);
assert_eq!(
shell.core_mut().resources.image_size(image),
Some((1, 1)),
"a resource the host registered draws in the window"
);
let mut core = Core::new();
core.set_devtools(true);
let mut shell = app("t")
.core(core)
.setup_core(|c| c.set_devtools(false))
.dyn_shell(Empty);
assert!(!shell.core_mut().devtools());
}
#[test]
fn the_devtools_chord_is_the_launcher_s_to_respell() {
let mut shell = app("t").diagnostics(false).dyn_shell(Empty);
assert_eq!(shell.core_mut().devtools_key().spelling(), "ctrl+shift+i");
let mut shell = app("t")
.diagnostics(false)
.devtools_key(Accel::parse("f12").unwrap())
.dyn_shell(Empty);
assert_eq!(shell.core_mut().devtools_key().spelling(), "f12");
}
#[test]
fn launcher_clamps_the_initial_size_into_the_bounds() {
let shell = app("t")
.size(320.0, 240.0)
.min_size(640.0, 480.0)
.dyn_shell(Empty);
assert_eq!(shell.size, (640.0, 480.0));
assert_eq!(shell.min_size, Some((640.0, 480.0)));
let shell = app("t")
.size(1600.0, 1200.0)
.max_size(800.0, 600.0)
.dyn_shell(Empty);
assert_eq!(shell.size, (800.0, 600.0));
assert_eq!(shell.max_size, Some((800.0, 600.0)));
let shell = app("t")
.min_size(640.0, 480.0)
.size(320.0, 240.0)
.dyn_shell(Empty);
assert_eq!(shell.size, (640.0, 480.0));
}
#[test]
fn a_reopen_waits_out_the_second_after_the_last_try() {
let now = std::time::Instant::now();
assert_eq!(reopen_due(None, now), now, "the first try is at once");
let recent = now - std::time::Duration::from_millis(300);
assert_eq!(
reopen_due(Some(recent), now),
recent + REOPEN_INTERVAL,
"inside the second: woken when it is up"
);
assert!(reopen_due(Some(recent), now) > now, "and not before");
let old = now - std::time::Duration::from_secs(5);
assert_eq!(reopen_due(Some(old), now), now, "past it: at once");
let edge = now - REOPEN_INTERVAL;
assert_eq!(reopen_due(Some(edge), now), now, "at it: at once");
}
#[test]
fn a_refused_surface_is_retried_then_given_up_once_and_the_count_saturates() {
let mut tries = 0u8;
let answers: Vec<Refused> = (0..1000).map(|_| surface_refused(&mut tries)).collect();
let retries = SURFACE_TRIES as usize;
assert!(answers[..retries].iter().all(|a| *a == Refused::Retry));
assert_eq!(answers[retries], Refused::GiveUp { say: true });
assert!(
answers[retries + 1..]
.iter()
.all(|a| *a == Refused::GiveUp { say: false }),
"given up, and said only the once"
);
assert_eq!(tries, u8::MAX);
}
#[test]
fn subpixel_text_is_decided_by_each_device() {
assert!(subpixel_on(TextAa::Auto, true));
assert!(!subpixel_on(TextAa::Auto, false));
assert!(subpixel_on(TextAa::Subpixel, true));
assert!(!subpixel_on(TextAa::Subpixel, false));
assert!(!subpixel_on(TextAa::Grayscale, true));
assert!(!subpixel_on(TextAa::Grayscale, false));
}
#[test]
fn a_backdrop_takes_auto_to_grayscale() {
assert_eq!(aa_under(TextAa::Auto, Backdrop::Opaque), TextAa::Auto);
for b in [Backdrop::Transparent, Backdrop::Blur, Backdrop::Tinted] {
assert_eq!(aa_under(TextAa::Auto, b), TextAa::Grayscale);
assert_eq!(aa_under(TextAa::Subpixel, b), TextAa::Subpixel);
}
assert!(!subpixel_on(aa_under(TextAa::Auto, Backdrop::Tinted), true));
}
#[test]
fn only_a_backdrop_opens_a_transparent_device() {
let shell = app("plain").dyn_shell(Empty);
assert!(!shell.gpu_options().transparent);
let shell = app("glass").backdrop(Backdrop::Blur).dyn_shell(Empty);
assert!(shell.gpu_options().transparent);
}
}