Files
endorphin-tessera-gui/src/app.rs
T
codex-bot 76e293d3b8
Verify Tessera GUI / verify (push) Failing after 7m25s
Add managed Window resource and native shell operations
2026-10-21 17:12:55 +08:00

570 lines
21 KiB
Rust

use std::{
num::NonZeroU32,
sync::{Arc, RwLock},
};
use softbuffer::{Context, Surface};
use winit::{
application::ApplicationHandler,
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
event::{ElementState, Ime, MouseButton, MouseScrollDelta, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
keyboard::{Key, ModifiersState, NamedKey},
window::{Window, WindowAttributes, WindowId},
};
use crate::{
activity::Activity,
developer_tools::DeveloperToolsState,
document::NamespaceState,
geometry::Point,
scene::{self, SceneLayout},
software_renderer::SoftwareRenderer,
};
#[derive(Clone, Debug)]
pub enum AppEvent {
RuntimeFailed { message: String },
DocumentUpdated,
ConsoleFailed { message: String, source: String },
}
pub struct TesseraApp {
window: Option<Arc<Window>>,
context: Option<Context<Arc<Window>>>,
surface: Option<Surface<Arc<Window>, Arc<Window>>>,
namespace: Arc<RwLock<NamespaceState>>,
developer_tools: DeveloperToolsState,
layout: SceneLayout,
pointer: Point,
modifiers: ModifiersState,
activity: Activity,
event_client: crate::events::Client,
guest_pressed: Option<crate::events::Input>,
guest_focus: Option<crate::events::Input>,
guest_key: Option<(NamedKey, crate::events::Input)>,
guest_keyboard: bool,
}
impl TesseraApp {
pub fn new(
namespace: Arc<RwLock<NamespaceState>>,
activity: Activity,
proxy: EventLoopProxy<AppEvent>,
) -> anyhow::Result<Self> {
let event_client = crate::events::Client::new(proxy.clone())?;
Ok(Self {
window: None,
context: None,
surface: None,
namespace,
developer_tools: {
let mut state = DeveloperToolsState::default();
state.proxy = Some(proxy);
state
},
layout: SceneLayout::default(),
pointer: Point::default(),
modifiers: ModifiersState::default(),
activity,
event_client,
guest_pressed: None,
guest_focus: None,
guest_key: None,
guest_keyboard: true,
})
}
fn apply_window_commands(&self) {
let Some(window) = &self.window else {
return;
};
let Ok(mut namespace) = self.namespace.write() else {
return;
};
let commands = std::mem::take(&mut namespace.window_commands);
for envelope in commands {
if envelope.session == 0
|| envelope.session != namespace.runtime_session
|| envelope.command.window != crate::window::MAIN_WINDOW
{
continue;
}
match envelope.command.action {
crate::window::Action::Resize { width, height } => {
let _ = window.request_inner_size(LogicalSize::new(width, height));
}
crate::window::Action::SetTitle(title) => window.set_title(&title),
crate::window::Action::Focus => window.focus_window(),
}
}
drop(namespace);
self.publish_viewport();
}
fn publish_viewport(&self) {
let Some(window) = &self.window else {
return;
};
let size = window.inner_size().to_logical::<f32>(window.scale_factor());
if size.width <= 0.0
|| size.height <= 0.0
|| !size.width.is_finite()
|| !size.height.is_finite()
{
return;
}
if let Ok(mut namespace) = self.namespace.write() {
namespace.viewport = Some(crate::layout::Size {
width: size.width.min(crate::ui_bridge::MAX_DIMENSION as f32),
height: size.height.min(crate::ui_bridge::MAX_DIMENSION as f32),
});
}
}
fn request_redraw(&self) {
if let Some(window) = &self.window {
window.request_redraw();
}
}
fn toggle_developer_tools(&mut self) {
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.toggle(&namespace.document);
}
self.guest_keyboard = !self.developer_tools.open;
self.request_redraw();
}
fn document_updated(&mut self) {
self.apply_window_commands();
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.document_updated(&namespace.document);
let frame = &namespace.document.frame;
self.guest_focus =
frame
.focus
.filter(|_| frame.fault.is_none())
.map(|mount| crate::events::Input {
session: frame.session,
layout_version: frame.layout_version,
mount,
kind: "focus".into(),
point: None,
press_point: None,
});
}
self.request_redraw();
}
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
if self.guest_keyboard && self.handle_guest_key(event) {
self.request_redraw();
return;
}
if self.developer_tools.keyboard_control(
event,
self.modifiers.shift_key(),
&self.layout.developer_tools,
&self.activity,
) {
self.request_redraw();
return;
}
if event.state != ElementState::Pressed {
return;
}
let is_f12 = matches!(event.logical_key, Key::Named(NamedKey::F12));
let is_i =
matches!(&event.logical_key, Key::Character(value) if value.eq_ignore_ascii_case("i"));
let mac_shortcut = self.modifiers.super_key() && self.modifiers.alt_key() && is_i;
let other_shortcut = self.modifiers.control_key() && self.modifiers.shift_key() && is_i;
if is_f12 || mac_shortcut || other_shortcut {
if !event.repeat {
self.toggle_developer_tools();
}
return;
}
if !self.developer_tools.open || self.developer_tools.focus.is_none() {
return;
}
let command = self.modifiers.super_key() || self.modifiers.control_key();
if command && let Key::Character(value) = &event.logical_key {
match value.to_lowercase().as_str() {
"a" => self.developer_tools.selection = true,
"v" => match crate::clipboard::read() {
Ok(value) => self.developer_tools.insert(&value),
Err(error) => self.developer_tools.input_error = format!("Clipboard: {error}"),
},
"c" | "x" => {
let selected = self.developer_tools.selected_text();
if !selected.is_empty() {
match crate::clipboard::write(&selected) {
Ok(()) if value.eq_ignore_ascii_case("x") => {
self.developer_tools.insert("")
}
Ok(()) => {}
Err(error) => {
self.developer_tools.input_error = format!("Clipboard: {error}")
}
}
}
}
_ => {}
}
} else if let Key::Named(key) = &event.logical_key {
if !self.developer_tools.preedit.is_empty() {
return;
}
self.developer_tools
.key(key, self.modifiers.shift_key(), command);
} else if !command
&& self.developer_tools.preedit.is_empty()
&& let Some(text) = &event.text
{
let text: String = text.chars().filter(|c| !c.is_control()).collect();
self.developer_tools.insert(&text);
}
self.request_redraw();
}
fn guest_covered(&self, point: Point) -> bool {
let panel = self.layout.developer_tools.panel;
self.developer_tools.open
&& crate::geometry::Rect::new(
panel.x - 6.0,
panel.y - 6.0,
panel.width + 12.0,
panel.height + 12.0,
)
.contains(point)
}
fn guest_hit(&self) -> Option<crate::events::Input> {
if self.guest_covered(self.pointer) {
return None;
}
let current = self.namespace.read().ok()?;
if current.viewport.is_some_and(|size| {
self.pointer.x < 0.0
|| self.pointer.y < 0.0
|| self.pointer.x >= size.width
|| self.pointer.y >= size.height
}) {
return None;
}
let frame = &current.document.frame;
if frame.session == 0 {
return None;
}
let mount = frame.hit_test(self.pointer)?;
Some(crate::events::Input {
session: frame.session,
layout_version: frame.layout_version,
mount: mount.id,
kind: "click".into(),
point: Some([self.pointer.x, self.pointer.y]),
press_point: Some([self.pointer.x, self.pointer.y]),
})
}
fn send_guest(&mut self, event: crate::events::Input) {
if !self.event_client.send(event) {
self.developer_tools.input_error = "Native event queue is unavailable or full".into();
self.developer_tools.open = true;
}
}
fn handle_guest_key(&mut self, event: &winit::event::KeyEvent) -> bool {
let Key::Named(key) = &event.logical_key else {
return false;
};
if *key == NamedKey::Tab {
if event.state == ElementState::Pressed && !event.repeat {
let next = self.namespace.read().ok().and_then(|namespace| {
let frame = &namespace.document.frame;
if frame.session == 0 || frame.fault.is_some() {
return None;
}
let targets: Vec<_> = frame
.mounts
.iter()
.filter(|mount| mount.interactive)
.collect();
if targets.is_empty() {
return None;
}
let current = self
.guest_focus
.as_ref()
.and_then(|focus| targets.iter().position(|mount| mount.id == focus.mount));
let index = if self.modifiers.shift_key() {
current
.map(|i| (i + targets.len() - 1) % targets.len())
.unwrap_or(targets.len() - 1)
} else {
current.map(|i| (i + 1) % targets.len()).unwrap_or(0)
};
Some(crate::events::Input {
session: frame.session,
layout_version: frame.layout_version,
mount: targets[index].id,
kind: "focus".into(),
point: None,
press_point: None,
})
});
if let Some(next) = next {
self.guest_key = None;
self.guest_focus = Some(next.clone());
self.send_guest(next);
return true;
}
return false;
}
return self.guest_focus.is_some();
}
if matches!(key, NamedKey::Enter | NamedKey::Space) && self.guest_focus.is_some() {
if event.state == ElementState::Pressed {
if !event.repeat {
self.guest_key = self.guest_focus.clone().map(|focus| (*key, focus));
}
} else if let Some((pressed, mut input)) = self.guest_key.take()
&& pressed == *key
{
input.kind = "click".into();
self.send_guest(input);
}
return true;
}
false
}
fn redraw(&mut self) {
let guest_cursor = self.guest_hit().is_some();
let (Some(window), Some(surface)) = (self.window.as_ref(), self.surface.as_mut()) else {
return;
};
let size = window.inner_size();
let (Some(width), Some(height)) =
(NonZeroU32::new(size.width), NonZeroU32::new(size.height))
else {
return;
};
let Ok(namespace) = self.namespace.read() else {
return;
};
let scene = scene::build_scene(
size.width,
size.height,
window.scale_factor() as f32,
&namespace.document,
&mut self.developer_tools,
&self.activity,
);
drop(namespace);
self.layout = scene.layout;
window.set_cursor(if guest_cursor {
winit::window::CursorIcon::Pointer
} else {
self.developer_tools
.cursor_kind(&self.layout.developer_tools)
});
if self.developer_tools.repaint {
self.developer_tools.repaint = false;
window.request_redraw();
}
let ime = self
.developer_tools
.ime_position(&self.layout.developer_tools);
window.set_ime_cursor_area(
LogicalPosition::new(ime.x as f64, ime.y as f64),
LogicalSize::new(1.0, 19.0),
);
surface
.resize(width, height)
.expect("resize software surface");
let mut buffer = surface.buffer_mut().expect("acquire software buffer");
SoftwareRenderer::render(&scene.display_list, &mut buffer, size.width, size.height);
buffer.present().expect("present software-rendered frame");
}
}
impl ApplicationHandler<AppEvent> for TesseraApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let attributes = WindowAttributes::default()
.with_title("Tessera")
.with_inner_size(LogicalSize::new(1_280.0, 800.0))
.with_min_inner_size(LogicalSize::new(720.0, 480.0))
.with_max_inner_size(LogicalSize::new(
crate::window::MAX_WINDOW_DIMENSION,
crate::window::MAX_WINDOW_DIMENSION,
));
let window = Arc::new(
event_loop
.create_window(attributes)
.expect("create Tessera window"),
);
let context = Context::new(window.clone()).expect("create software-rendering context");
let surface =
Surface::new(&context, window.clone()).expect("create software-rendering surface");
window.set_ime_allowed(true);
self.window = Some(window.clone());
self.context = Some(context);
self.surface = Some(surface);
self.apply_window_commands();
self.publish_viewport();
window.request_redraw();
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
match event {
AppEvent::RuntimeFailed { message } => {
self.developer_tools.input_error = message;
self.developer_tools.open = true;
self.guest_pressed = None;
self.guest_key = None;
self.request_redraw();
}
AppEvent::DocumentUpdated => self.document_updated(),
AppEvent::ConsoleFailed { message, source } => {
self.developer_tools.input_error = format!("Request failed: {message}");
if self.developer_tools.input.is_empty() {
self.developer_tools.input = source;
self.developer_tools.cursor = self.developer_tools.input.len();
self.developer_tools.reveal_cursor();
}
self.request_redraw();
}
}
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let Some(window) = self.window.as_ref().cloned() else {
return;
};
if window.id() != window_id {
return;
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
self.publish_viewport();
window.request_redraw();
}
WindowEvent::CursorMoved {
position: PhysicalPosition { x, y },
..
} => {
let scale = window.scale_factor() as f32;
let next = Point::new(x as f32 / scale, y as f32 / scale);
let delta = Point::new(next.x - self.pointer.x, next.y - self.pointer.y);
self.pointer = next;
self.developer_tools.move_pointer(
next,
delta,
&self.layout.developer_tools,
window.inner_size().width as f32 / scale,
);
window.set_cursor(if self.guest_hit().is_some() {
winit::window::CursorIcon::Pointer
} else {
self.developer_tools
.cursor_kind(&self.layout.developer_tools)
});
window.request_redraw();
}
WindowEvent::CursorLeft { .. } => {
self.guest_pressed = None;
self.developer_tools.pointer_left();
window.set_cursor(winit::window::CursorIcon::Default);
window.request_redraw();
}
WindowEvent::MouseInput {
state,
button: MouseButton::Left,
..
} => {
if state == ElementState::Pressed {
self.guest_keyboard = !self.guest_covered(self.pointer);
self.guest_pressed = self.guest_hit();
if self.guest_keyboard {
self.developer_tools.blur();
}
self.developer_tools.press(
&self.layout.developer_tools,
self.pointer,
&self.activity,
);
} else {
if let Some(pressed) = self.guest_pressed.take()
&& let Some(mut released) = self.guest_hit()
&& released.session == pressed.session
&& released.layout_version == pressed.layout_version
{
released.press_point = pressed.point;
self.send_guest(released);
}
self.developer_tools.release(
&self.layout.developer_tools,
self.pointer,
&self.activity,
);
}
window.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
let (mut dx, mut dy) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * 38.0, y * 38.0),
MouseScrollDelta::PixelDelta(p) => (
p.x as f32 / window.scale_factor() as f32,
p.y as f32 / window.scale_factor() as f32,
),
};
if self.modifiers.shift_key() && dx.abs() < 0.1 {
dx = dy;
dy = 0.0;
}
self.developer_tools
.wheel(&self.layout.developer_tools, self.pointer, dx, dy);
window.request_redraw();
}
WindowEvent::Ime(Ime::Preedit(value, _)) => {
self.developer_tools.preedit = value;
window.request_redraw();
}
WindowEvent::Ime(Ime::Commit(value)) => {
self.developer_tools.preedit.clear();
if self.developer_tools.open {
self.developer_tools.insert(&value);
}
window.request_redraw();
}
WindowEvent::Focused(false) => {
self.guest_pressed = None;
self.guest_key = None;
self.developer_tools.blur();
self.modifiers = ModifiersState::default();
window.request_redraw();
}
WindowEvent::ModifiersChanged(modifiers) => self.modifiers = modifiers.state(),
WindowEvent::KeyboardInput { event, .. } => self.handle_keyboard(&event),
WindowEvent::RedrawRequested => self.redraw(),
_ => {}
}
}
}