use crate::app::input;
pub use crate::app::options::SurfaceOptions;
use crate::app::safe_area;
use crate::app::touch::TouchState;
use crate::app::visibility::FrameVisibility;
use crate::app::wake::{ProxyWaker, Wake};
use crate::core::{CursorStyle, InputEvent, Point, Size, px};
use crate::gpu::Renderer;
use crate::view::window::Window as UiWindow;
use std::sync::Arc;
use std::time::Instant;
use winit::dpi::{LogicalPosition, LogicalSize};
use winit::event::{ElementState, Ime, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use winit::keyboard::ModifiersState;
use winit::window::{Window, WindowAttributes, WindowId};
const LINE_SCROLL: f32 = 60.0;
const MOBILE: bool = cfg!(any(target_os = "android", target_os = "ios"));
pub struct Surface {
pub window: Arc<Window>,
pub renderer: Renderer,
pub ui: UiWindow,
pub access: accesskit_winit::Adapter,
pub modifiers: ModifiersState,
pub visibility: FrameVisibility,
pub frames_drawn: u32,
pub touch: TouchState,
cursor: CursorStyle,
ime_allowed: bool,
atlas_reset_frame: u32,
}
impl Surface {
pub fn create(
event_loop: &ActiveEventLoop,
proxy: &EventLoopProxy<Wake>,
options: SurfaceOptions,
) -> Result<Self, Box<dyn std::error::Error>> {
let (width, height) = options.size;
let attributes = Window::default_attributes().with_title(&options.title);
let attributes = if MOBILE { attributes } else { attributes.with_inner_size(LogicalSize::new(width, height)) };
#[cfg(target_os = "macos")]
let attributes = if options.custom_titlebar {
use winit::platform::macos::WindowAttributesExtMacOS;
attributes.with_titlebar_transparent(true).with_fullsize_content_view(true).with_title_hidden(true)
} else {
attributes
};
let mut ui = UiWindow::new(options.theme, Size::new(px(width as f32), px(height as f32)), 1.0);
ui.rem_size = options.rem_size;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if options.system_clipboard {
ui.entities.clipboard = Box::new(crate::app::clipboard::SystemClipboard::new());
}
Self::attach(event_loop, proxy, attributes, ui)
}
pub fn reopen(
event_loop: &ActiveEventLoop,
proxy: &EventLoopProxy<Wake>,
ui: UiWindow,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::attach(event_loop, proxy, Window::default_attributes(), ui)
}
pub fn into_ui(mut self) -> UiWindow {
self.ui.shrink_caches();
self.ui
}
fn attach(
event_loop: &ActiveEventLoop,
proxy: &EventLoopProxy<Wake>,
attributes: WindowAttributes,
mut ui: UiWindow,
) -> Result<Self, Box<dyn std::error::Error>> {
let window = Arc::new(event_loop.create_window(attributes.with_visible(false))?);
let access = accesskit_winit::Adapter::with_event_loop_proxy(event_loop, &window, proxy.clone());
window.set_visible(true);
#[cfg(target_os = "ios")]
crate::app::ios_scene::adopt(&window);
window.set_ime_allowed(!MOBILE);
let size = safe_area::surface_size(&window);
let renderer = crate::app::renderer_choice::create(event_loop, &window, (size.width, size.height))?;
let scale = window.scale_factor() as f32;
ui.resize(Size::new(px(size.width as f32 / scale), px(size.height as f32 / scale)), scale);
ui.set_safe_area(safe_area::insets(&window));
ui.entities.inbox.set_waker(Arc::new(ProxyWaker { proxy: proxy.clone(), window: window.id() }));
Ok(Self {
window,
renderer,
ui,
access,
modifiers: ModifiersState::default(),
visibility: FrameVisibility::new(size.width, size.height),
frames_drawn: 0,
touch: TouchState::default(),
cursor: CursorStyle::Arrow,
ime_allowed: !MOBILE,
atlas_reset_frame: 0,
})
}
pub fn id(&self) -> WindowId {
self.window.id()
}
pub fn scale(&self) -> f32 {
self.window.scale_factor() as f32
}
pub fn request_redraw(&self) {
if self.visibility.is_drawable() {
self.window.request_redraw();
}
}
fn logical(&self, x: f64, y: f64) -> Point {
let scale = self.scale() as f64;
Point::new(px((x / scale) as f32), px((y / scale) as f32))
}
pub fn handle(&mut self, event: WindowEvent) -> bool {
match event {
WindowEvent::Resized(_) => {
let (size, scale) = (safe_area::surface_size(&self.window), self.scale());
self.ui.set_safe_area(safe_area::insets(&self.window));
self.visibility.resize(size.width, size.height);
if self.visibility.is_drawable() {
self.renderer.resize(size.width, size.height);
}
self.ui.resize(Size::new(px(size.width as f32 / scale), px(size.height as f32 / scale)), scale);
true
}
WindowEvent::ScaleFactorChanged { .. } | WindowEvent::Focused(_) => true,
WindowEvent::Occluded(occluded) => {
let changed = self.visibility.set_occluded(occluded);
if changed && occluded {
self.ui.shrink_caches();
self.renderer.shrink_caches();
} else if changed && self.visibility.is_drawable() {
let size = safe_area::surface_size(&self.window);
self.renderer.resize(size.width, size.height);
}
changed && !occluded
}
WindowEvent::ModifiersChanged(modifiers) => {
self.modifiers = modifiers.state();
self.ui.set_modifiers(input::modifiers(self.modifiers));
false
}
WindowEvent::CursorMoved { position, .. } => {
let position = self.logical(position.x, position.y);
self.ui.handle_event(&InputEvent::MouseMove { position })
}
WindowEvent::MouseInput { state, button, .. } => {
let Some(button) = input::mouse_button(button) else { return false };
let position = self.ui.mouse_position();
let event = match state {
ElementState::Pressed => InputEvent::MouseDown { position, button },
ElementState::Released => InputEvent::MouseUp { position, button },
};
self.ui.handle_event(&event)
}
WindowEvent::MouseWheel { delta, .. } => {
let scale = self.scale() as f64;
let (x, y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * LINE_SCROLL, y * LINE_SCROLL),
MouseScrollDelta::PixelDelta(pixels) => ((pixels.x / scale) as f32, (pixels.y / scale) as f32),
};
let position = self.ui.mouse_position();
self.ui.handle_event(&InputEvent::Scroll { position, delta: Point::new(px(x), px(y)) })
}
WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
let Some(keystroke) = input::keystroke(&event, self.modifiers) else { return false };
let text = event.text.as_ref().map(|text| text.to_string());
self.ui.handle_event(&InputEvent::KeyDown { keystroke, text })
}
WindowEvent::Ime(Ime::Preedit(text, cursor)) => {
self.ui.handle_event(&InputEvent::ImePreedit { text, cursor: cursor.map(|(start, _)| start) })
}
WindowEvent::Ime(Ime::Commit(text)) => self.ui.handle_event(&InputEvent::ImeCommit(text)),
WindowEvent::Touch(touch) => {
let position = self.logical(touch.location.x, touch.location.y);
let events = self.touch.touch(touch.phase, touch.id, position, Instant::now());
self.dispatch(&events)
}
_ => false,
}
}
pub fn dispatch(&mut self, events: &[InputEvent]) -> bool {
events.iter().fold(false, |redraw, event| self.ui.handle_event(event) | redraw)
}
pub fn wants_frame(&mut self) -> bool {
self.touch.is_flinging() || self.ui.wants_animation_frame()
}
pub fn redraw(&mut self) -> Result<(), crate::gpu::context::Error> {
let scale = self.scale();
if let Some(scroll) = self.touch.step_fling(Instant::now()) {
self.ui.handle_event(&scroll);
}
self.ui.draw(Some(self.renderer.glyph_target()));
let clear = self.ui.theme.canvas;
self.renderer.render(self.ui.scene(), scale, clear)?;
let area = self.ui.ime_area();
if MOBILE && area.is_some() != self.ime_allowed {
self.ime_allowed = area.is_some();
self.window.set_ime_allowed(self.ime_allowed);
}
if let Some(area) = area {
self.window.set_ime_cursor_area(
LogicalPosition::new(area.origin.x.0, area.origin.y.0),
LogicalSize::new(area.size.width.0, area.size.height.0),
);
}
self.frames_drawn += 1;
self.relieve_atlas_overflow();
self.sync_cursor();
self.publish_access();
Ok(())
}
fn relieve_atlas_overflow(&mut self) {
if !self.renderer.take_atlas_overflow() || self.frames_drawn.saturating_sub(self.atlas_reset_frame) <= 2 {
return;
}
self.atlas_reset_frame = self.frames_drawn;
self.ui.shrink_caches();
self.renderer.shrink_caches();
self.request_redraw();
}
fn sync_cursor(&mut self) {
let cursor = self.ui.cursor_style();
if cursor != self.cursor {
self.cursor = cursor;
self.window.set_cursor(input::cursor_icon(cursor));
}
}
pub fn access_event(&mut self, event: accesskit_winit::WindowEvent) {
match event {
accesskit_winit::WindowEvent::InitialTreeRequested => {
self.ui.set_accessibility(true);
self.ui.draw(None);
self.publish_access();
}
accesskit_winit::WindowEvent::ActionRequested(request) => {
if self.ui.accessibility_action(&request) {
self.request_redraw();
}
}
accesskit_winit::WindowEvent::AccessibilityDeactivated => self.ui.set_accessibility(false),
}
}
pub fn publish_access(&mut self) {
if self.ui.accessibility_active() {
let ui = &self.ui;
self.access.update_if_active(|| ui.accessibility_tree());
}
}
}