use crate::BlitzShellProvider;
use crate::convert_events::{
button_source_to_blitz, color_scheme_to_theme, pointer_source_to_blitz,
pointer_source_to_blitz_details, theme_to_color_scheme, winit_ime_to_blitz,
winit_key_event_to_blitz, winit_modifiers_to_kbt_modifiers,
};
use crate::event::{BlitzShellProxy, create_waker};
use anyrender::WindowRenderer;
use blitz_dom::Document;
use blitz_paint::paint_scene;
use blitz_traits::events::{
BlitzPointerEvent, BlitzPointerId, BlitzWheelDelta, BlitzWheelEvent, MouseEventButton,
MouseEventButtons, PointerCoords, PointerDetails, UiEvent,
};
use blitz_traits::shell::Viewport;
use winit::dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition};
use winit::keyboard::PhysicalKey;
use std::any::Any;
use std::sync::Arc;
use std::task::Waker;
use std::time::Instant;
use winit::event::{ButtonSource, ElementState, MouseButton};
use winit::event_loop::ActiveEventLoop;
use winit::window::{Theme, WindowAttributes, WindowId};
use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window};
#[cfg(feature = "accessibility")]
use crate::accessibility::AccessibilityState;
pub struct WindowConfig<Rend: WindowRenderer> {
doc: Box<dyn Document>,
attributes: WindowAttributes,
renderer: Rend,
}
impl<Rend: WindowRenderer> WindowConfig<Rend> {
pub fn new(doc: Box<dyn Document>, renderer: Rend) -> Self {
Self::with_attributes(doc, renderer, WindowAttributes::default())
}
pub fn with_attributes(
doc: Box<dyn Document>,
renderer: Rend,
attributes: WindowAttributes,
) -> Self {
WindowConfig {
doc,
attributes,
renderer,
}
}
}
pub struct View<Rend: WindowRenderer> {
pub doc: Box<dyn Document>,
pub renderer: Rend,
pub waker: Option<Waker>,
pub proxy: BlitzShellProxy,
pub window: Arc<dyn Window>,
pub theme_override: Option<Theme>,
pub keyboard_modifiers: Modifiers,
pub buttons: MouseEventButtons,
pub pointer_pos: PhysicalPosition<f64>,
pub animation_timer: Option<Instant>,
pub is_visible: bool,
pub safe_area_insets: PhysicalInsets<u32>,
#[cfg(feature = "accessibility")]
pub accessibility: AccessibilityState,
#[cfg(target_os = "ios")]
pub ios_request_redraw: std::cell::Cell<bool>,
}
impl<Rend: WindowRenderer> View<Rend> {
pub fn init(
config: WindowConfig<Rend>,
event_loop: &dyn ActiveEventLoop,
proxy: &BlitzShellProxy,
) -> Self {
let is_visible = config.attributes.visible;
let attrs = config.attributes.with_visible(false);
let winit_window: Arc<dyn Window> = Arc::from(event_loop.create_window(attrs).unwrap());
#[cfg(feature = "accessibility")]
let accessibility = AccessibilityState::new(&*winit_window, proxy.clone());
if is_visible {
winit_window.set_visible(true);
}
let size = winit_window.surface_size();
let scale = winit_window.scale_factor() as f32;
let safe_area_insets = winit_window.safe_area();
let theme = winit_window.theme().unwrap_or(Theme::Light);
let color_scheme = theme_to_color_scheme(theme);
let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
let shell_provider = BlitzShellProvider::new(winit_window.clone());
let mut doc = config.doc;
let mut inner = doc.inner_mut();
inner.set_viewport(viewport);
inner.set_shell_provider(Arc::new(shell_provider));
let title = inner.find_title_node().map(|node| node.text_content());
if let Some(title) = title {
winit_window.set_title(&title);
}
drop(inner);
Self {
renderer: config.renderer,
waker: None,
animation_timer: None,
keyboard_modifiers: Default::default(),
proxy: proxy.clone(),
window: winit_window.clone(),
doc,
theme_override: None,
buttons: MouseEventButtons::None,
safe_area_insets,
pointer_pos: Default::default(),
is_visible: winit_window.is_visible().unwrap_or(true),
#[cfg(feature = "accessibility")]
accessibility,
#[cfg(target_os = "ios")]
ios_request_redraw: std::cell::Cell::new(false),
}
}
pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
let inner = self.doc.inner();
let scroll = inner.viewport_scroll();
let viewport = inner.viewport().clone();
let shell_provider = inner.shell_provider.clone();
drop(inner);
self.doc = new_doc;
let mut inner = self.doc.inner_mut();
inner.set_viewport(viewport);
inner.set_shell_provider(shell_provider);
drop(inner);
self.poll();
self.request_redraw();
if retain_scroll_position {
self.doc.inner_mut().set_viewport_scroll(scroll);
}
}
pub fn theme_override(&self) -> Option<Theme> {
self.theme_override
}
pub fn current_theme(&self) -> Theme {
color_scheme_to_theme(self.doc.inner().viewport().color_scheme)
}
pub fn set_theme_override(&mut self, theme: Option<Theme>) {
self.theme_override = theme;
let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
}
pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
(&mut *self.doc as &mut dyn Any)
.downcast_mut::<T>()
.unwrap()
}
pub fn current_animation_time(&mut self) -> f64 {
match &self.animation_timer {
Some(start) => Instant::now().duration_since(*start).as_secs_f64(),
None => {
self.animation_timer = Some(Instant::now());
0.0
}
}
}
}
impl<Rend: WindowRenderer> View<Rend> {
pub fn resume(&mut self) {
let window_id = self.window_id();
let animation_time = self.current_animation_time();
let mut inner = self.doc.inner_mut();
inner.resolve(animation_time);
let (width, height) = inner.viewport().window_size;
let scale = inner.viewport().scale_f64();
self.renderer
.resume(Arc::new(self.window.clone()), width, height);
if !self.renderer.is_active() {
panic!("Renderer failed to resume");
};
let insets = self.safe_area_insets.to_logical(scale);
self.renderer.render(|scene| {
paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
});
self.waker = Some(create_waker(&self.proxy, window_id));
}
pub fn suspend(&mut self) {
self.waker = None;
self.renderer.suspend();
}
pub fn poll(&mut self) -> bool {
if let Some(waker) = &self.waker {
let cx = std::task::Context::from_waker(waker);
if self.doc.poll(Some(cx)) {
#[cfg(feature = "accessibility")]
{
let inner = self.doc.inner();
if inner.has_changes() {
self.accessibility.update_tree(&inner);
}
}
self.request_redraw();
return true;
}
}
false
}
pub fn request_redraw(&self) {
if self.renderer.is_active() {
self.window.request_redraw();
#[cfg(target_os = "ios")]
self.ios_request_redraw.set(true);
}
}
pub fn redraw(&mut self) {
#[cfg(target_os = "ios")]
self.ios_request_redraw.set(false);
let animation_time = self.current_animation_time();
let is_visible = self.is_visible;
let mut inner = self.doc.inner_mut();
inner.resolve(animation_time);
let (width, height) = inner.viewport().window_size;
let scale = inner.viewport().scale_f64();
let is_animating = inner.is_animating();
let is_blocked = inner.has_pending_critical_resources();
let insets = self.safe_area_insets.to_logical(scale);
if !is_blocked && is_visible {
self.renderer.render(|scene| {
paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
});
}
drop(inner);
if !is_blocked && is_visible && is_animating {
self.request_redraw();
}
}
pub fn pointer_coords(&self, position: PhysicalPosition<f64>) -> PointerCoords {
let inner = self.doc.inner();
let scale = inner.viewport().scale_f64();
let LogicalPosition::<f32> {
x: screen_x,
y: screen_y,
} = position.to_logical(scale);
let viewport_scroll_offset = inner.viewport_scroll();
let client_x = screen_x - (self.safe_area_insets.left as f64 / scale) as f32;
let client_y = screen_y - (self.safe_area_insets.top as f64 / scale) as f32;
let page_x = client_x + viewport_scroll_offset.x as f32;
let page_y = client_y + viewport_scroll_offset.y as f32;
PointerCoords {
screen_x,
screen_y,
client_x,
client_y,
page_x,
page_y,
}
}
pub fn window_id(&self) -> WindowId {
self.window.id()
}
#[inline]
pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
let mut inner = self.doc.inner_mut();
let mut viewport = inner.viewport_mut();
cb(&mut viewport);
let (width, height) = viewport.window_size;
drop(viewport);
drop(inner);
if width > 0 && height > 0 {
let insets = self.safe_area_insets;
self.renderer.set_size(
width + insets.left + insets.right,
height + insets.top + insets.bottom,
);
self.request_redraw();
}
}
#[cfg(feature = "accessibility")]
pub fn build_accessibility_tree(&mut self) {
let inner = self.doc.inner();
self.accessibility.update_tree(&inner);
}
#[cfg(target_os = "macos")]
pub fn handle_apple_standard_keybinding(&mut self, command: &str) {
use blitz_traits::SmolStr;
let event = UiEvent::AppleStandardKeybinding(SmolStr::new(command));
self.doc.handle_ui_event(event);
}
pub fn handle_winit_event(&mut self, event: WindowEvent) {
#[cfg(feature = "accessibility")]
self.accessibility
.process_window_event(&*self.window, &event);
match event {
WindowEvent::Destroyed => {}
WindowEvent::ActivationTokenDone { .. } => {},
WindowEvent::CloseRequested => {
}
WindowEvent::RedrawRequested => {
self.redraw();
}
WindowEvent::Moved(_) => {}
WindowEvent::Occluded(is_occluded) => {
self.is_visible = !is_occluded;
if self.is_visible {
self.request_redraw();
}
},
WindowEvent::SurfaceResized(physical_size) => {
self.safe_area_insets = self.window.safe_area();
let insets = self.safe_area_insets;
let width = physical_size.width - insets.left - insets.right;
let height = physical_size.height - insets.top - insets.bottom;
self.with_viewport(|v| v.window_size = (width, height));
self.request_redraw();
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
self.request_redraw();
}
WindowEvent::ThemeChanged(theme) => {
let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
let mut inner = self.doc.inner_mut();
inner.viewport_mut().color_scheme = color_scheme;
}
WindowEvent::Ime(ime_event) => {
self.doc.handle_ui_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
self.request_redraw();
},
WindowEvent::ModifiersChanged(new_state) => {
self.keyboard_modifiers = new_state;
}
WindowEvent::KeyboardInput { event, .. } => {
if let PhysicalKey::Code(key_code) = event.physical_key && event.state.is_pressed() {
let ctrl = self.keyboard_modifiers.state().control_key();
let meta = self.keyboard_modifiers.state().meta_key();
let alt = self.keyboard_modifiers.state().alt_key();
if ctrl | meta {
match key_code {
KeyCode::Equal => {
self.doc.inner_mut().viewport_mut().zoom_by(0.1);
},
KeyCode::Minus => {
self.doc.inner_mut().viewport_mut().zoom_by(-0.1);
},
KeyCode::Digit0 => {
self.doc.inner_mut().viewport_mut().set_zoom(1.0);
}
_ => {}
};
}
if alt {
match key_code {
KeyCode::KeyD => {
let mut inner = self.doc.inner_mut();
inner.devtools_mut().toggle_show_layout();
drop(inner);
self.request_redraw();
}
KeyCode::KeyH => {
let mut inner = self.doc.inner_mut();
inner.devtools_mut().toggle_highlight_hover();
drop(inner);
self.request_redraw();
}
KeyCode::KeyT => self.doc.inner().print_taffy_tree(),
_ => {}
};
}
}
let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
let event = if event.state.is_pressed() {
UiEvent::KeyDown(key_event_data)
} else {
UiEvent::KeyUp(key_event_data)
};
self.doc.handle_ui_event(event);
}
WindowEvent::PointerEntered { .. } => {}
WindowEvent::PointerLeft { .. } => {}
WindowEvent::PointerMoved { position, source, primary, .. } => {
self.pointer_pos = position;
let event = UiEvent::PointerMove(BlitzPointerEvent {
id: pointer_source_to_blitz(&source),
is_primary: primary,
coords: self.pointer_coords(position),
button: Default::default(),
buttons: self.buttons,
mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
details: pointer_source_to_blitz_details(&source)
});
self.doc.handle_ui_event(event);
}
WindowEvent::PointerButton { button, state, primary, position, .. } => {
let id = button_source_to_blitz(&button);
let coords = self.pointer_coords(position);
self.pointer_pos = position;
let button = match &button {
ButtonSource::Mouse(mouse_button) => match mouse_button {
MouseButton::Left => MouseEventButton::Main,
MouseButton::Right => MouseEventButton::Secondary,
MouseButton::Middle => MouseEventButton::Auxiliary,
_ => MouseEventButton::Auxiliary,
}
_ => MouseEventButton::Main,
};
match state {
ElementState::Pressed => self.buttons |= button.into(),
ElementState::Released => self.buttons ^= button.into(),
}
if id != BlitzPointerId::Mouse {
let event = UiEvent::PointerMove(BlitzPointerEvent {
id,
is_primary: primary,
coords,
button: Default::default(),
buttons: self.buttons,
mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
details: PointerDetails::default()
});
self.doc.handle_ui_event(event);
}
let event = BlitzPointerEvent {
id,
is_primary: primary,
coords,
button,
buttons: self.buttons,
mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
details: PointerDetails::default(),
};
let event = match state {
ElementState::Pressed => UiEvent::PointerDown(event),
ElementState::Released => UiEvent::PointerUp(event),
};
self.doc.handle_ui_event(event);
self.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
let blitz_delta = match delta {
winit::event::MouseScrollDelta::LineDelta(x, y) => BlitzWheelDelta::Lines(x as f64, y as f64),
winit::event::MouseScrollDelta::PixelDelta(pos) => BlitzWheelDelta::Pixels(pos.x, pos.y),
};
let event = BlitzWheelEvent {
delta: blitz_delta,
coords: self.pointer_coords(self.pointer_pos),
buttons: self.buttons,
mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
};
self.doc.handle_ui_event(UiEvent::Wheel(event));
}
WindowEvent::Focused(_) => {}
WindowEvent::TouchpadPressure { .. } => {}
WindowEvent::PinchGesture { .. } => {},
WindowEvent::PanGesture { .. } => {},
WindowEvent::DoubleTapGesture { .. } => {},
WindowEvent::RotationGesture { .. } => {},
WindowEvent::DragEntered { .. } => {},
WindowEvent::DragMoved { .. } => {},
WindowEvent::DragDropped { .. } => {},
WindowEvent::DragLeft { .. } => {},
}
}
}