use super::*;
use winit::keyboard::PhysicalKey;
pub(crate) fn sync_env(
pane: &mut Pane,
system: &system_env::Queried,
pinned: SystemEnv,
audio: AudioEnv,
) {
let window = &pane.window;
let chrome = pane.chrome;
pane.core.env.refresh_hz = window
.current_monitor()
.and_then(|m| m.refresh_rate_millihertz())
.map(|mhz| mhz as f32 / 1000.0);
pane.core.env.system = system_reading(
pane.appearance,
system,
pinned,
pane.access
.as_ref()
.map_or(Assistive::Unknown, |b| b.assistive()),
);
pane.core.env.window = WindowEnv {
id: pane.id,
custom_chrome: chrome != Chrome::Native,
maximized: window.is_maximized(),
fullscreen: window.fullscreen().is_some(),
always_on_top: pane.applied_on_top && pane.level_supported,
native_controls: pane.native_controls,
backdrop: pane.backdrop,
};
pane.core.env.audio = audio;
}
pub(crate) fn system_reading(
appearance: Appearance,
queried: &system_env::Queried,
pinned: SystemEnv,
assistive: Assistive,
) -> SystemEnv {
pinned.over(SystemEnv {
appearance,
accent: queried.accent,
motion: queried.motion,
locale: queried.locale,
assistive,
})
}
pub(crate) fn appearance_of(window: &Window) -> Appearance {
theme_appearance(window.theme())
}
pub(crate) fn theme_appearance(theme: Option<winit::window::Theme>) -> Appearance {
match theme {
Some(winit::window::Theme::Light) => Appearance::Light,
Some(winit::window::Theme::Dark) => Appearance::Dark,
None => Appearance::Unknown,
}
}
pub(crate) fn level_change(
kind: WindowKind,
want: bool,
applied: &mut bool,
) -> Option<winit::window::WindowLevel> {
if kind == WindowKind::Popup || want == *applied {
return None;
}
*applied = want;
Some(if want {
winit::window::WindowLevel::AlwaysOnTop
} else {
winit::window::WindowLevel::Normal
})
}
pub(crate) fn option_as_alt_change(
want: kui_core::OptionAsAlt,
applied: &mut kui_core::OptionAsAlt,
) -> Option<kui_core::OptionAsAlt> {
if want == *applied {
return None;
}
*applied = want;
Some(want)
}
pub(crate) fn ime_off_change(want: bool, applied: &mut bool) -> Option<bool> {
if want == *applied {
return None;
}
*applied = want;
Some(want)
}
pub(crate) fn apply_ime_off(window: &Window, off: bool) {
#[cfg(target_os = "macos")]
if off {
crate::macos_text_input::discard_marked_text(window);
}
window.set_ime_allowed(!off);
}
pub(crate) fn note_ime_key(
held: &mut Vec<PhysicalKey>,
ime_off: bool,
key: PhysicalKey,
pressed: bool,
repeat: bool,
) -> bool {
if pressed {
if ime_off && !repeat && !held.contains(&key) {
held.push(key);
}
return false;
}
let before = held.len();
held.retain(|k| *k != key);
before > 0 && held.is_empty()
}
pub(crate) fn option_is_alt(applied: kui_core::OptionAsAlt, held: (bool, bool)) -> bool {
use kui_core::KeyLocation::{Left, Right};
(held.0 && applied.covers(Left)) || (held.1 && applied.covers(Right))
}
pub(crate) fn level_supported(window: &Window) -> bool {
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
!matches!(
window.window_handle().map(|h| h.as_raw()),
Ok(RawWindowHandle::Wayland(_))
)
}
pub(crate) fn cursor_icon(shape: CursorShape) -> CursorIcon {
match shape {
CursorShape::Default => CursorIcon::Default,
CursorShape::Text => CursorIcon::Text,
CursorShape::Pointer => CursorIcon::Pointer,
CursorShape::Grab => CursorIcon::Grab,
CursorShape::Grabbing => CursorIcon::Grabbing,
CursorShape::NotAllowed => CursorIcon::NotAllowed,
CursorShape::EwResize => CursorIcon::EwResize,
CursorShape::NsResize => CursorIcon::NsResize,
CursorShape::NwseResize => CursorIcon::NwseResize,
CursorShape::NeswResize => CursorIcon::NeswResize,
}
}
pub(crate) struct Pane {
pub(crate) id: WindowId,
pub(crate) kind: WindowKind,
pub(crate) owner: WindowId,
pub(crate) activates: bool,
pub(crate) chrome: Chrome,
pub(crate) backdrop: Backdrop,
pub(crate) ground: Option<crate::ground::Ground>,
pub(crate) _backdrop_keep: Option<Box<dyn std::any::Any>>,
pub(crate) native_controls: Option<Rect>,
pub(crate) applied_min: Option<(f64, f64)>,
pub(crate) anchor: Rect,
pub(crate) handed_back: bool,
pub(crate) os_focused: bool,
pub(crate) appearance: Appearance,
pub(crate) core: Core,
pub(crate) window: Arc<Window>,
pub(crate) renderer: Option<kui_wgpu::Renderer>,
pub(crate) surface_tries: u8,
pub(crate) awaits_device: bool,
pub(crate) applied_title: String,
pub(crate) applied_on_top: bool,
pub(crate) level_supported: bool,
pub(crate) applied_option_as_alt: kui_core::OptionAsAlt,
pub(crate) applied_ime_off: bool,
pub(crate) ime_off_held: Vec<PhysicalKey>,
pub(crate) preedit_open: bool,
pub(crate) modifiers: ModifiersState,
pub(crate) modifier_keys_down: Vec<crate::keys::HeldModifier>,
pub(crate) alt_held: (bool, bool),
pub(crate) last_titlebar_press: Option<std::time::Instant>,
pub(crate) cursor: Vec2,
pub(crate) pressure_stage: i64,
pub(crate) axis_lock: crate::axis_lock::AxisLock,
pub(crate) scroll_gesture: crate::scroll_gesture::Gesture,
pub(crate) file_drag: Vec<String>,
pub(crate) file_drag_pending: Option<bool>,
pub(crate) last_click: Option<(std::time::Instant, Vec2, u8)>,
pub(crate) blink_visible: bool,
pub(crate) blink_deadline: Option<std::time::Instant>,
pub(crate) caret_stamp_seen: u64,
pub(crate) resize_edge: Option<ResizeDirection>,
pub(crate) cursor_icon: CursorIcon,
pub(crate) retry: crate::retry::Retry,
pub(crate) deferred_frame: bool,
pub(crate) cause: Causes,
pub(crate) pacer: crate::pacer::Pacer,
pub(crate) access: Option<access_bridge::Bridge>,
#[cfg(target_os = "windows")]
pub(crate) anim_timer: Option<windows_anim::AnimTimer>,
#[cfg(target_os = "windows")]
pub(crate) nc: Option<windows_nc::NcHitTest>,
}
impl Pane {
pub(crate) fn ime_off_wanted(&self) -> bool {
self.core.ime_off() || !self.ime_off_held.is_empty()
}
pub(crate) fn sync_ime(&mut self) {
if let Some(off) = ime_off_change(self.ime_off_wanted(), &mut self.applied_ime_off) {
apply_ime_off(&self.window, off);
}
}
pub(crate) fn note_ime_key(&mut self, key: PhysicalKey, pressed: bool, repeat: bool) {
if note_ime_key(
&mut self.ime_off_held,
self.applied_ime_off,
key,
pressed,
repeat,
) {
self.sync_ime();
}
}
pub(crate) fn minimized(&self) -> bool {
cfg!(target_os = "windows") && self.window.is_minimized() == Some(true)
}
pub(crate) fn animates_now(&self) -> bool {
self.core.animating() && !self.awaits_device && !self.minimized() && !self.cause.is_dark()
}
pub(crate) fn redraw_for(&self, why: FrameCause) {
self.cause.note(why);
self.window.request_redraw();
}
pub(crate) fn came_back(&self, why: FrameCause) {
self.cause.came_back(why);
self.window.request_redraw();
}
pub(crate) fn admit_frame(&mut self) -> bool {
let px = self.window.inner_size();
self.pacer
.admit(std::time::Instant::now(), (px.width, px.height))
}
pub(crate) fn size(&self) -> (Size, f32) {
let scale = self.window.scale_factor() as f32;
let size = self.window.inner_size();
(
Size::new(size.width as f32 / scale, size.height as f32 / scale),
scale,
)
}
pub(crate) fn surface(&self) -> Option<retarget::Surface> {
let origin = self.window.inner_position().ok()?;
let (size, scale) = self.size();
if cfg!(target_os = "macos") {
let origin = origin.to_logical::<f64>(scale as f64);
return Some(retarget::Surface {
origin: (origin.x, origin.y),
scale: 1.0,
size,
});
}
Some(retarget::Surface {
origin: (origin.x as f64, origin.y as f64),
scale: scale as f64,
size,
})
}
pub(crate) fn synthesizes_resize(&self) -> bool {
#[cfg(target_os = "windows")]
if self.nc.is_some() {
return false;
}
self.chrome != Chrome::Native
&& self.kind != WindowKind::Popup
&& !cfg!(target_os = "macos")
}
pub(crate) fn resize_edge_at(&self, p: Vec2) -> Option<ResizeDirection> {
let w = &self.window;
if w.is_maximized() || w.fullscreen().is_some() {
return None;
}
let (size, _) = self.size();
let (sw, sh) = (size.w, size.h);
let (l, r) = (p.x < RESIZE_BAND, p.x > sw - RESIZE_BAND);
let (t, b) = (p.y < RESIZE_BAND, p.y > sh - RESIZE_BAND);
Some(match (l, r, t, b) {
(true, _, true, _) => ResizeDirection::NorthWest,
(_, true, true, _) => ResizeDirection::NorthEast,
(true, _, _, true) => ResizeDirection::SouthWest,
(_, true, _, true) => ResizeDirection::SouthEast,
(true, ..) => ResizeDirection::West,
(_, true, ..) => ResizeDirection::East,
(_, _, true, _) => ResizeDirection::North,
(_, _, _, true) => ResizeDirection::South,
_ => return None,
})
}
pub(crate) fn apply_cursor(&mut self) {
let icon = match self.resize_edge {
Some(ResizeDirection::West | ResizeDirection::East) => CursorIcon::EwResize,
Some(ResizeDirection::North | ResizeDirection::South) => CursorIcon::NsResize,
Some(ResizeDirection::NorthWest | ResizeDirection::SouthEast) => CursorIcon::NwseResize,
Some(ResizeDirection::NorthEast | ResizeDirection::SouthWest) => CursorIcon::NeswResize,
None => cursor_icon(self.core.cursor_shape()),
};
if icon == self.cursor_icon {
return;
}
self.cursor_icon = icon;
self.window.set_cursor(icon);
}
pub(crate) fn publish_access(&mut self) {
let said = self.core.take_announcements();
let Some(bridge) = self.access.as_mut() else {
return;
};
if !bridge.active() {
return;
}
let scale = self.window.scale_factor() as f32;
bridge.publish(self.core.access_tree(), scale, &said);
}
pub(crate) fn primary(&self) -> bool {
if cfg!(target_os = "macos") {
self.modifiers.super_key()
} else {
self.modifiers.control_key()
}
}
pub(crate) fn modifier_key(
&mut self,
code: kui_core::KeyCode,
at: kui_core::KeyCode,
location: kui_core::KeyLocation,
pressed: bool,
mods: KeyMods,
) -> KeyMods {
crate::keys::modifier_after(
&mut self.modifier_keys_down,
code,
at,
location,
pressed,
mods,
)
}
pub(crate) fn kmods(&self) -> KeyMods {
KeyMods {
shift: self.modifiers.shift_key(),
ctrl: self.modifiers.control_key(),
alt: self.modifiers.alt_key(),
super_key: self.modifiers.super_key(),
}
}
}
#[derive(Default)]
pub(crate) struct Causes {
noted: std::cell::Cell<FrameCause>,
dark: std::cell::Cell<bool>,
noted_dark: std::cell::Cell<FrameCause>,
}
impl Causes {
pub(crate) fn new(first: FrameCause) -> Causes {
let c = Causes::default();
c.noted.set(first);
c
}
pub(crate) fn note(&self, why: FrameCause) {
self.noted.set(self.noted.get() | why);
if self.dark.get() {
self.noted_dark.set(self.noted_dark.get() | why);
}
}
pub(crate) fn take(&self) -> FrameCause {
self.noted_dark.take();
self.noted.take()
}
pub(crate) fn went_dark(&self) {
self.dark.set(true);
}
pub(crate) fn is_dark(&self) -> bool {
self.dark.get()
}
pub(crate) fn came_back(&self, why: FrameCause) {
let stale = self.noted_dark.take().bits();
self.dark.set(false);
self.noted
.set(FrameCause::from_bits(self.noted.get().bits() & !stale) | why);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_frame_back_from_the_dark_names_the_return() {
let c = Causes::new(FrameCause::FIRST);
assert_eq!(c.take(), FrameCause::FIRST);
c.note(FrameCause::KEY);
c.went_dark();
c.note(FrameCause::ELSEWHERE);
c.note(FrameCause::APPEARANCE);
c.came_back(FrameCause::OCCLUSION);
assert_eq!(c.take(), FrameCause::KEY | FrameCause::OCCLUSION);
c.went_dark();
c.note(FrameCause::WAKE);
assert_eq!(c.take(), FrameCause::WAKE);
c.note(FrameCause::ELSEWHERE);
c.came_back(FrameCause::OCCLUSION);
assert_eq!(c.take(), FrameCause::OCCLUSION);
assert!(!c.is_dark());
c.note(FrameCause::HOST);
c.note(FrameCause::HOST);
assert_eq!(c.take(), FrameCause::HOST);
}
#[test]
fn a_pinned_field_beats_the_query_and_the_rest_stay_the_oss() {
let queried = system_env::Queried {
accent: Some(Color::hex(0x0a84ffff)),
motion: MotionPref::Full,
locale: Locale::new("en-US"),
};
let none = SystemEnv::default();
assert_eq!(
system_reading(Appearance::Light, &queried, none, Assistive::Unknown),
SystemEnv {
appearance: Appearance::Light,
accent: queried.accent,
motion: MotionPref::Full,
locale: queried.locale,
assistive: Assistive::Unknown,
}
);
assert_eq!(
system_reading(Appearance::Light, &queried, none, Assistive::Listening).assistive,
Assistive::Listening
);
let less_motion = SystemEnv {
motion: MotionPref::Reduced,
..Default::default()
};
let read = system_reading(Appearance::Light, &queried, less_motion, Assistive::Unknown);
assert_eq!(
read.motion,
MotionPref::Reduced,
"the pin wins over a real Full"
);
assert_eq!(read.appearance, Appearance::Light);
assert_eq!(read.accent, queried.accent);
assert_eq!(read.locale, queried.locale);
let dark = system_reading(Appearance::Dark, &queried, less_motion, Assistive::Unknown);
assert_eq!(dark.appearance, Appearance::Dark);
assert_eq!(dark.motion, MotionPref::Reduced);
let mute = system_env::Queried::default();
assert_eq!(
system_reading(Appearance::Unknown, &mute, less_motion, Assistive::Unknown).motion,
MotionPref::Reduced
);
}
#[test]
fn a_level_is_applied_on_change_and_never_for_a_popup() {
use winit::window::WindowLevel;
let mut applied = false;
assert_eq!(
level_change(WindowKind::Normal, true, &mut applied),
Some(WindowLevel::AlwaysOnTop)
);
assert!(applied);
assert_eq!(level_change(WindowKind::Normal, true, &mut applied), None);
assert_eq!(level_change(WindowKind::Normal, true, &mut applied), None);
assert_eq!(
level_change(WindowKind::Normal, false, &mut applied),
Some(WindowLevel::Normal)
);
assert!(!applied);
assert_eq!(level_change(WindowKind::Normal, false, &mut applied), None);
let mut popup = true;
assert_eq!(level_change(WindowKind::Popup, false, &mut popup), None);
assert_eq!(level_change(WindowKind::Popup, true, &mut popup), None);
assert!(popup);
}
#[test]
fn option_as_alt_is_applied_on_change() {
use kui_core::OptionAsAlt::{Both, Left, None as Off};
let mut applied = Off;
assert_eq!(option_as_alt_change(Off, &mut applied), None);
assert_eq!(option_as_alt_change(Left, &mut applied), Some(Left));
assert_eq!(applied, Left);
assert_eq!(option_as_alt_change(Left, &mut applied), None);
assert_eq!(option_as_alt_change(Left, &mut applied), None);
assert_eq!(option_as_alt_change(Both, &mut applied), Some(Both));
assert_eq!(option_as_alt_change(Off, &mut applied), Some(Off));
assert_eq!(applied, Off);
assert_eq!(option_as_alt_change(Off, &mut applied), None);
}
#[test]
fn ime_off_is_applied_on_change() {
let mut applied = false;
assert_eq!(ime_off_change(false, &mut applied), None);
assert_eq!(ime_off_change(true, &mut applied), Some(true));
assert!(applied);
assert_eq!(ime_off_change(true, &mut applied), None);
assert_eq!(ime_off_change(true, &mut applied), None);
assert_eq!(ime_off_change(false, &mut applied), Some(false));
assert!(!applied);
assert_eq!(ime_off_change(false, &mut applied), None);
}
#[test]
fn a_key_pressed_with_the_ime_off_holds_it_off_until_released() {
use winit::keyboard::{KeyCode, PhysicalKey::Code};
let mut held = Vec::new();
assert!(!note_ime_key(
&mut held,
true,
Code(KeyCode::KeyI),
true,
false
));
assert_eq!(held, [Code(KeyCode::KeyI)]);
assert!(!note_ime_key(
&mut held,
false,
Code(KeyCode::KeyI),
true,
true
));
assert!(!note_ime_key(
&mut held,
true,
Code(KeyCode::ShiftLeft),
true,
false
));
assert!(!note_ime_key(
&mut held,
true,
Code(KeyCode::ShiftLeft),
false,
false
));
assert_eq!(
held,
[Code(KeyCode::KeyI)],
"a repeat records nothing twice"
);
assert!(note_ime_key(
&mut held,
false,
Code(KeyCode::KeyI),
false,
false
));
assert!(held.is_empty());
assert!(!note_ime_key(
&mut held,
false,
Code(KeyCode::KeyE),
true,
false
));
assert!(!note_ime_key(
&mut held,
false,
Code(KeyCode::KeyE),
false,
false
));
assert!(held.is_empty());
}
#[test]
fn only_a_covered_option_is_alt() {
use kui_core::OptionAsAlt;
let (left, right, neither) = ((true, false), (false, true), (false, false));
assert!(option_is_alt(OptionAsAlt::Left, left));
assert!(!option_is_alt(OptionAsAlt::Left, right));
assert!(option_is_alt(OptionAsAlt::Right, right));
assert!(option_is_alt(OptionAsAlt::Both, left));
assert!(option_is_alt(OptionAsAlt::Both, right));
assert!(!option_is_alt(OptionAsAlt::None, left));
assert!(!option_is_alt(OptionAsAlt::None, (true, true)));
assert!(!option_is_alt(OptionAsAlt::Both, neither));
}
}