Acord/viewport/src/handle.rs

774 lines
28 KiB
Rust

use std::ffi::c_void;
use std::ptr::NonNull;
use iced_graphics::{Shell, Viewport};
use iced_runtime::user_interface::{self, UserInterface};
use iced_wgpu::core::renderer::Style;
use iced_wgpu::core::time::Instant;
use iced_wgpu::core::{clipboard, keyboard, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme};
use iced_wgpu::Engine;
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
#[cfg(target_os = "macos")]
use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle};
#[cfg(target_os = "ios")]
use raw_window_handle::{UiKitDisplayHandle, UiKitWindowHandle};
#[cfg(target_os = "windows")]
use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle};
use crate::editor::{EditorState, Message, RenderMode};
use crate::palette;
use crate::table_block::TableMessage;
use crate::ViewportHandle;
#[cfg(all(not(target_os = "ios"), feature = "native-shell"))]
struct AcordClipboard {
board: std::cell::RefCell<arboard::Clipboard>,
}
#[cfg(all(not(target_os = "ios"), feature = "native-shell"))]
impl clipboard::Clipboard for AcordClipboard {
fn read(&self, _kind: clipboard::Kind) -> Option<String> {
let mut board = self.board.borrow_mut();
// encode pasteboard bitmap to PNG and yield a markdown image reference
if let Ok(img) = board.get_image() {
if let Some(path) = crate::editor::write_clipboard_image_to_cache(&img) {
return Some(format!("\n![]({})\n", path));
}
}
// normalize \r\n to \n
board.get_text()
.ok()
.map(|s| s.replace("\r\n", "\n").replace('\r', "\n"))
}
fn write(&mut self, _kind: clipboard::Kind, contents: String) {
let _ = self.board.borrow_mut().set_text(contents);
}
}
/// stub clipboard for iOS and embedded consumers without native-shell.
#[cfg(any(target_os = "ios", not(feature = "native-shell")))]
struct AcordClipboard;
#[cfg(any(target_os = "ios", not(feature = "native-shell")))]
impl clipboard::Clipboard for AcordClipboard {
fn read(&self, _kind: clipboard::Kind) -> Option<String> { None }
fn write(&mut self, _kind: clipboard::Kind, _contents: String) {}
}
/// synthesizes platform display+window handles from a single native pointer.
pub fn create(
native_handle: *mut c_void,
width: f32,
height: f32,
scale: f32,
) -> Option<ViewportHandle> {
let ptr = NonNull::new(native_handle)?;
#[cfg(target_os = "macos")]
let (raw_window, raw_display) = (
RawWindowHandle::AppKit(AppKitWindowHandle::new(ptr)),
RawDisplayHandle::AppKit(AppKitDisplayHandle::new()),
);
#[cfg(target_os = "ios")]
let (raw_window, raw_display) = (
RawWindowHandle::UiKit(UiKitWindowHandle::new(ptr)),
RawDisplayHandle::UiKit(UiKitDisplayHandle::new()),
);
#[cfg(target_os = "windows")]
let (raw_window, raw_display) = {
let wh = Win32WindowHandle::new(std::num::NonZero::new(ptr.as_ptr() as isize).unwrap());
(
RawWindowHandle::Win32(wh),
RawDisplayHandle::Windows(WindowsDisplayHandle::new()),
)
};
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
{
let _ = (ptr, width, height, scale);
return None;
}
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "windows"))]
create_native(raw_display, raw_window, width, height, scale)
}
/// creates the wgpu surface, device, queue, renderer, and viewport from typed handles.
pub fn create_native(
raw_display: RawDisplayHandle,
raw_window: RawWindowHandle,
width: f32,
height: f32,
scale: f32,
) -> Option<ViewportHandle> {
#[cfg(any(target_os = "macos", target_os = "ios"))]
let backends = wgpu::Backends::METAL;
#[cfg(target_os = "windows")]
let backends = wgpu::Backends::DX12;
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
let backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends,
..Default::default()
});
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle: raw_display,
raw_window_handle: raw_window,
};
let surface = unsafe { instance.create_surface_unsafe(target).ok()? };
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}))
.ok()?;
let (device, queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()?;
let phys_w = (width * scale) as u32;
let phys_h = (height * scale) as u32;
let caps = surface.get_capabilities(&adapter);
let format = caps.formats.first().copied()?;
let alpha_mode = caps.alpha_modes.first().copied().unwrap_or(wgpu::CompositeAlphaMode::Auto);
crate::ios_dlog!(
"surface formats={:?} chose={:?} alpha_modes={:?} chose={:?} adapter={}",
caps.formats, format, caps.alpha_modes, alpha_mode,
adapter.get_info().name,
);
surface.configure(
&device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: phys_w.max(1),
height: phys_h.max(1),
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
let engine = Engine::new(
&adapter,
device.clone(),
queue.clone(),
format,
None,
Shell::headless(),
);
// Font::DEFAULT = cosmic-text "Noto Sans Mono", absent on iOS; EDITOR_FONT overrides per-span
crate::ios_dlog!("renderer init font=Font::DEFAULT (= cosmic 'Noto Sans Mono', iOS likely missing) editor_font={:?}", crate::syntax::EDITOR_FONT);
let renderer = iced_wgpu::Renderer::new(engine, Font::DEFAULT, Pixels(16.0));
let viewport =
Viewport::with_physical_size(Size::new(phys_w.max(1), phys_h.max(1)), scale);
let focus_point = Point::new(width / 2.0, height / 2.0);
let initial_events = vec![
Event::Mouse(mouse::Event::CursorMoved { position: focus_point }),
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
];
Some(ViewportHandle {
surface,
device,
queue,
format,
width: phys_w,
height: phys_h,
scale,
renderer,
viewport,
cache: user_interface::Cache::new(),
state: EditorState::new(),
events: initial_events,
cursor: mouse::Cursor::Available(focus_point),
needs_redraw: true,
})
}
pub fn render(handle: &mut ViewportHandle) {
// skip frame entirely when idle and no eval debounce pending
let pending_events = !handle.events.is_empty();
if !handle.needs_redraw && !handle.state.has_pending_eval() && !pending_events {
return;
}
let frame = match handle.surface.get_current_texture() {
Ok(f) => f,
Err(_) => return,
};
let view = frame.texture.create_view(&Default::default());
let logical_size = handle.viewport.logical_size();
handle
.events
.push(Event::Window(window::Event::RedrawRequested(Instant::now())));
let cache = std::mem::take(&mut handle.cache);
let mut ui = UserInterface::build(
handle.state.view(),
Size::new(logical_size.width, logical_size.height),
cache,
&mut handle.renderer,
);
#[cfg(all(not(target_os = "ios"), feature = "native-shell"))]
let mut clipboard = AcordClipboard {
board: std::cell::RefCell::new(arboard::Clipboard::new().unwrap()),
};
#[cfg(any(target_os = "ios", not(feature = "native-shell")))]
let mut clipboard = AcordClipboard;
let mut messages: Vec<Message> = Vec::new();
let mut consumed: Vec<usize> = Vec::new();
let mut latest_mods: Option<keyboard::Modifiers> = None;
let mut new_cmd_a_armed: Option<bool> = None;
for (ev_idx, event) in handle.events.iter().enumerate() {
let is_user_input = matches!(
event,
Event::Keyboard(keyboard::Event::KeyPressed { .. })
| Event::Mouse(mouse::Event::ButtonPressed(_))
);
if is_user_input {
new_cmd_a_armed = Some(false);
}
if handle.state.render_mode == RenderMode::View {
let is_typing_char = match event {
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c), modifiers, ..
}) if !modifiers.logo() && !modifiers.control() && !modifiers.alt() => {
c.as_str() != "i" && c.as_str() != "/"
}
_ => false,
};
let is_destructive_named = matches!(
event,
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(
keyboard::key::Named::Backspace
| keyboard::key::Named::Delete
| keyboard::key::Named::Enter
| keyboard::key::Named::Tab
),
..
})
);
if is_typing_char || is_destructive_named {
consumed.push(ev_idx);
continue;
}
}
match event {
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c),
modifiers,
..
}) if modifiers.logo() => {
match c.as_str() {
"p" => {
messages.push(Message::TogglePreview);
consumed.push(ev_idx);
}
"t" => {
messages.push(Message::InsertTable);
consumed.push(ev_idx);
}
"a" => {
if handle.state.cmd_a_armed {
messages.push(Message::SelectAllBlocks);
new_cmd_a_armed = Some(false);
consumed.push(ev_idx);
} else {
if handle.state.table_is_focused_block()
&& !handle.state.focused_table_is_select_all()
&& handle.state.editing.is_none()
{
messages.push(Message::FocusedTableOp(
TableMessage::SelectAll,
));
consumed.push(ev_idx);
}
new_cmd_a_armed = Some(true);
}
}
"b" => {
messages.push(Message::ToggleBold);
consumed.push(ev_idx);
}
"i" => {
messages.push(Message::ToggleItalic);
consumed.push(ev_idx);
}
"u" => {
messages.push(Message::ToggleUnderline);
consumed.push(ev_idx);
}
"x" if modifiers.shift() => {
messages.push(Message::ToggleStrike);
consumed.push(ev_idx);
}
"." if modifiers.shift() => {
messages.push(Message::ToggleBlockquote);
consumed.push(ev_idx);
}
"\"" | "'" => {
let q: &'static str = if c.as_str() == "\"" { "\"" } else { "'" };
messages.push(Message::WrapWith(q, q));
consumed.push(ev_idx);
}
"9" | "(" => {
messages.push(Message::WrapWith("(", ")"));
consumed.push(ev_idx);
}
"0" if modifiers.shift() => {
messages.push(Message::ZoomReset);
consumed.push(ev_idx);
}
"0" => {
messages.push(Message::FixUp);
consumed.push(ev_idx);
}
"c" => {
if handle.state.should_intercept_table_copy() {
messages.push(Message::CopyFocusedTableSelection);
consumed.push(ev_idx);
}
}
"e" => {
messages.push(Message::SmartEval);
consumed.push(ev_idx);
}
"r" => {
messages.push(Message::EvalAll);
consumed.push(ev_idx);
}
"z" if modifiers.shift() => {
messages.push(Message::Redo);
consumed.push(ev_idx);
}
"z" => {
messages.push(Message::Undo);
consumed.push(ev_idx);
}
"f" => {
messages.push(Message::ToggleFind);
consumed.push(ev_idx);
}
"g" if modifiers.shift() => {
messages.push(Message::FindPrev);
consumed.push(ev_idx);
}
"g" => {
messages.push(Message::FindNext);
consumed.push(ev_idx);
}
_ => {}
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c),
modifiers,
..
}) if modifiers.control() && !modifiers.logo() => {
match c.as_str() {
"i" => {
messages.push(Message::SetRenderMode(RenderMode::Editor));
consumed.push(ev_idx);
}
"/" => {
messages.push(Message::SetRenderMode(RenderMode::Live));
consumed.push(ev_idx);
}
_ => {}
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
modifiers,
..
}) if modifiers.control() => {
messages.push(Message::SetRenderMode(RenderMode::View));
consumed.push(ev_idx);
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c),
modifiers,
..
}) if handle.state.render_mode == RenderMode::View
&& !modifiers.logo() && !modifiers.control() && !modifiers.alt() => {
match c.as_str() {
"i" => {
messages.push(Message::SetRenderMode(RenderMode::Editor));
consumed.push(ev_idx);
}
"/" => {
messages.push(Message::SetRenderMode(RenderMode::Live));
consumed.push(ev_idx);
}
_ => {}
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(named),
modifiers,
..
}) if modifiers.logo() && modifiers.alt() => {
use keyboard::key::Named;
let op = match named {
Named::ArrowUp => Some(TableMessage::InsertRowAbove),
Named::ArrowDown => Some(TableMessage::InsertRowBelow),
Named::ArrowLeft => Some(TableMessage::InsertColLeft),
Named::ArrowRight => Some(TableMessage::InsertColRight),
Named::Backspace if modifiers.shift() => Some(TableMessage::DeleteCol),
Named::Backspace => Some(TableMessage::DeleteRow),
_ => None,
};
if let Some(tmsg) = op {
messages.push(Message::FocusedTableOp(tmsg));
consumed.push(ev_idx);
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(named),
modifiers,
..
}) if !modifiers.logo() && !modifiers.alt() && !modifiers.control()
&& handle.state.table_is_focused_block() =>
{
use keyboard::key::Named;
match named {
Named::Tab if modifiers.shift() => {
messages.push(Message::TableShiftTab);
consumed.push(ev_idx);
}
Named::Tab => {
messages.push(Message::TableTab);
consumed.push(ev_idx);
}
Named::Enter => {
messages.push(Message::TableEnter);
consumed.push(ev_idx);
}
Named::ArrowUp => {
if let Some((block_idx, row, _)) =
handle.state.active_table_focused_row()
{
if row == 0 {
messages.push(Message::EscapeTableUp(block_idx));
} else {
messages.push(Message::TableMoveUp);
}
consumed.push(ev_idx);
}
}
Named::ArrowDown => {
if let Some((block_idx, row, total)) =
handle.state.active_table_focused_row()
{
if row + 1 >= total {
messages.push(Message::EscapeTableDown(block_idx));
} else {
messages.push(Message::TableMoveDown);
}
consumed.push(ev_idx);
}
}
Named::ArrowLeft => {
messages.push(Message::TableMoveLeft);
consumed.push(ev_idx);
}
Named::ArrowRight => {
messages.push(Message::TableMoveRight);
consumed.push(ev_idx);
}
Named::Backspace | Named::Delete
if handle.state.focused_table_is_select_all() =>
{
messages.push(Message::FocusedTableOp(TableMessage::ClearAll));
consumed.push(ev_idx);
}
Named::Backspace | Named::Delete
if handle.state.has_selected_cell_not_editing() =>
{
messages.push(Message::ClearSelectedCell);
consumed.push(ev_idx);
}
_ => {}
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Backspace),
modifiers,
..
}) if modifiers.logo() && !modifiers.alt() && !modifiers.control()
&& handle.state.all_blocks_selected =>
{
messages.push(Message::DeleteAllBlocks);
consumed.push(ev_idx);
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Backspace),
modifiers,
..
}) if modifiers.logo() && !modifiers.alt() && !modifiers.control()
&& handle.state.focused_table_is_select_all() =>
{
messages.push(Message::DeleteCurrentTable);
consumed.push(ev_idx);
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(named),
modifiers,
..
}) if (matches!(named, keyboard::key::Named::Backspace | keyboard::key::Named::Delete))
&& !modifiers.logo() && !modifiers.alt() && !modifiers.control()
&& handle.state.all_blocks_selected =>
{
messages.push(Message::ClearAllBlocks);
consumed.push(ev_idx);
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
modifiers,
..
}) if !modifiers.control() => {
if handle.state.context_menu.is_some() {
messages.push(Message::HideContextMenu);
consumed.push(ev_idx);
} else if handle.state.find.visible {
messages.push(Message::HideFind);
consumed.push(ev_idx);
} else if handle.state.editing.is_some() {
messages.push(Message::ExitCellEdit);
consumed.push(ev_idx);
} else {
match handle.state.render_mode {
RenderMode::Live => {
messages.push(Message::SetRenderMode(RenderMode::Editor));
consumed.push(ev_idx);
}
RenderMode::Editor => {
messages.push(Message::SetRenderMode(RenderMode::View));
consumed.push(ev_idx);
}
RenderMode::View => {
messages.push(Message::SetRenderMode(RenderMode::Live));
consumed.push(ev_idx);
}
}
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c),
modifiers,
..
}) if !modifiers.logo() && !modifiers.alt() && !modifiers.control()
&& handle.state.has_selected_cell_not_editing() =>
{
if let Some(first) = c.chars().next() {
if !first.is_control() {
messages.push(Message::EnterCellEditWithChar(first));
consumed.push(ev_idx);
}
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(mods)) => {
latest_mods = Some(*mods);
}
_ => {}
}
}
// remove consumed events before passing the remainder to iced
if !consumed.is_empty() {
let consumed_set: std::collections::HashSet<usize> = consumed.into_iter().collect();
let mut filtered: Vec<Event> = Vec::with_capacity(handle.events.len());
for (i, e) in handle.events.drain(..).enumerate() {
if !consumed_set.contains(&i) {
filtered.push(e);
}
}
handle.events = filtered;
}
let _ = ui.update(
&handle.events,
handle.cursor,
&mut handle.renderer,
&mut clipboard,
&mut messages,
);
handle.events.clear();
let focused_id = {
use iced_wgpu::core::widget::operation::{Focusable, Operation};
use iced_wgpu::core::widget::Id as CoreId;
use iced_wgpu::core::Rectangle;
struct FindFocusedId {
focused: Option<CoreId>,
}
impl Operation<()> for FindFocusedId {
fn focusable(
&mut self,
id: Option<&CoreId>,
_bounds: Rectangle,
state: &mut dyn Focusable,
) {
if state.is_focused() && id.is_some() && self.focused.is_none() {
self.focused = id.cloned();
}
}
fn traverse(
&mut self,
operate: &mut dyn FnMut(&mut dyn Operation<()>),
) {
operate(self);
}
fn container(&mut self, _id: Option<&CoreId>, _bounds: Rectangle) {}
}
let mut op = FindFocusedId { focused: None };
ui.operate(&handle.renderer, &mut op);
op.focused
};
let cache = ui.into_cache();
if let Some(mods) = latest_mods {
handle.state.mods = mods;
}
if let Some(armed) = new_cmd_a_armed {
handle.state.cmd_a_armed = armed;
}
if let Some(pt) = handle.cursor.position() {
handle.state.cursor_pos = pt;
if handle.state.tick_promote_drag() {
handle.needs_redraw = true;
}
if handle.state.tick_resize_drag() {
handle.needs_redraw = true;
}
}
handle.state.sync_focused_cell(focused_id.as_ref());
for msg in messages.drain(..) {
handle.state.update(msg);
}
#[cfg(all(not(target_os = "ios"), feature = "native-shell"))]
if let Some(text) = handle.state.pending_clipboard.take() {
if let Ok(mut board) = arboard::Clipboard::new() {
let _ = board.set_text(text);
}
}
#[cfg(any(target_os = "ios", not(feature = "native-shell")))]
let _ = handle.state.pending_clipboard.take();
handle.state.tick();
let pending_focus = handle.state.take_pending_focus();
let pending_scroll = handle.state.take_pending_scroll();
let theme = Theme::Dark;
let style = Style {
text_color: Color::WHITE,
};
{
use std::sync::atomic::{AtomicU64, Ordering};
static FRAME: AtomicU64 = AtomicU64::new(0);
let n = FRAME.fetch_add(1, Ordering::Relaxed);
if n == 0 || n == 60 || n % 600 == 0 {
let p = palette::current();
let _ = (n, &p, &theme, &style);
crate::ios_dlog!(
"render frame={n} theme=Dark style.text={:?} palette.base={:?} palette.text={:?} palette.surface0={:?}",
style.text_color,
p.base,
p.text,
p.surface0,
);
}
}
let mut ui = UserInterface::build(
handle.state.view(),
Size::new(logical_size.width, logical_size.height),
cache,
&mut handle.renderer,
);
if let Some(focus_id) = pending_focus {
use iced_wgpu::core::widget::operation::focusable;
let mut op = focusable::focus(focus_id);
ui.operate(&handle.renderer, &mut op);
}
// forward accumulated wheel-scroll delta to the document scrollable
if let Some(delta_y) = pending_scroll {
use iced_wgpu::core::widget::operation::scrollable::{scroll_by, AbsoluteOffset};
use iced_wgpu::core::widget::Id as CoreId;
let mut op = scroll_by::<()>(
CoreId::new(crate::editor::DOC_SCROLLABLE_ID),
AbsoluteOffset { x: 0.0, y: delta_y },
);
ui.operate(&handle.renderer, &mut op);
}
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache();
handle
.renderer
.present(Some(palette::current().base), handle.format, &view, &handle.viewport);
frame.present();
handle.needs_redraw = false;
}
pub fn resize(handle: &mut ViewportHandle, width: f32, height: f32, scale: f32) {
let phys_w = (width * scale) as u32;
let phys_h = (height * scale) as u32;
if phys_w == 0 || phys_h == 0 {
return;
}
handle.width = phys_w;
handle.height = phys_h;
handle.scale = scale;
handle.viewport = Viewport::with_physical_size(Size::new(phys_w, phys_h), scale);
handle.surface.configure(
&handle.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: handle.format,
width: phys_w,
height: phys_h,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
}