replace Swift editor with Iced TextEditor, delete old compositor

This commit is contained in:
jess 2026-04-07 17:52:23 -07:00
parent 50aad4bf84
commit 7a2aa08d1c
9 changed files with 174 additions and 4645 deletions

View File

@ -24,8 +24,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
_ = ConfigManager.shared _ = ConfigManager.shared
appState = AppState() appState = AppState()
let contentView = ContentView(state: appState) let viewport = IcedViewportView(frame: NSRect(x: 0, y: 0, width: 1200, height: 800))
let hostingView = NSHostingView(rootView: contentView) viewport.autoresizingMask = [.width, .height]
window = NSWindow( window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800), contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
@ -37,7 +37,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.backgroundColor = Theme.current.base window.backgroundColor = Theme.current.base
window.title = "Swiftly" window.title = "Swiftly"
window.contentView = hostingView window.contentView = viewport
window.center() window.center()
window.setFrameAutosaveName("SwiftlyMainWindow") window.setFrameAutosaveName("SwiftlyMainWindow")
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
@ -235,8 +235,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc private func newWindow() { @objc private func newWindow() {
let state = AppState() let state = AppState()
let contentView = ContentView(state: state) let viewport = IcedViewportView(frame: NSRect(x: 0, y: 0, width: 1200, height: 800))
let hostingView = NSHostingView(rootView: contentView) viewport.autoresizingMask = [.width, .height]
let win = NSWindow( let win = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800), contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
@ -248,7 +248,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
win.titleVisibility = .hidden win.titleVisibility = .hidden
win.backgroundColor = Theme.current.base win.backgroundColor = Theme.current.base
win.title = "Swiftly" win.title = "Swiftly"
win.contentView = hostingView win.contentView = viewport
win.center() win.center()
win.makeKeyAndOrderFront(nil) win.makeKeyAndOrderFront(nil)

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,7 @@ struct ContentView: View {
var body: some View { var body: some View {
let _ = themeVersion let _ = themeVersion
HSplitView {
EditorView(state: state)
.frame(minWidth: 400)
IcedViewportRepresentable() IcedViewportRepresentable()
.frame(minWidth: 200)
}
.frame(minWidth: 700, minHeight: 400) .frame(minWidth: 700, minHeight: 400)
.background(Color(ns: Theme.current.base)) .background(Color(ns: Theme.current.base))
.onReceive(NotificationCenter.default.publisher(for: .settingsChanged)) { _ in .onReceive(NotificationCenter.default.publisher(for: .settingsChanged)) { _ in

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ class IcedViewportView: NSView {
if window != nil && viewportHandle == nil { if window != nil && viewportHandle == nil {
createViewport() createViewport()
startDisplayLink() startDisplayLink()
window?.makeFirstResponder(self)
} else if window == nil { } else if window == nil {
stopDisplayLink() stopDisplayLink()
destroyViewport() destroyViewport()
@ -105,6 +106,7 @@ class IcedViewportView: NSView {
// MARK: - Mouse Events // MARK: - Mouse Events
override func mouseDown(with event: NSEvent) { override func mouseDown(with event: NSEvent) {
window?.makeFirstResponder(self)
guard let h = viewportHandle else { return } guard let h = viewportHandle else { return }
let pt = convert(event.locationInWindow, from: nil) let pt = convert(event.locationInWindow, from: nil)
viewport_mouse_event(h, Float(pt.x), Float(pt.y), 0, true) viewport_mouse_event(h, Float(pt.x), Float(pt.y), 0, true)

View File

@ -1 +0,0 @@
import SwiftUI

View File

@ -6,7 +6,7 @@ use smol_str::SmolStr;
use crate::ViewportHandle; use crate::ViewportHandle;
pub fn push_mouse_event(handle: &mut ViewportHandle, x: f32, y: f32, button: u8, pressed: bool) { pub fn push_mouse_event(handle: &mut ViewportHandle, x: f32, y: f32, button: u8, pressed: bool) {
let position = Point::new(x / handle.scale, y / handle.scale); let position = Point::new(x, y);
handle.cursor = mouse::Cursor::Available(position); handle.cursor = mouse::Cursor::Available(position);
handle.events.push(Event::Mouse(mouse::Event::CursorMoved { position })); handle.events.push(Event::Mouse(mouse::Event::CursorMoved { position }));
@ -34,10 +34,21 @@ pub fn push_key_event(
) { ) {
let modifiers = decode_modifiers(modifier_flags); let modifiers = decode_modifiers(modifier_flags);
let physical = key::Physical::Unidentified(key::NativeCode::MacOS(keycode as u16)); let physical = key::Physical::Unidentified(key::NativeCode::MacOS(keycode as u16));
let logical = text
.filter(|s| !s.is_empty()) let named = keycode_to_named(keycode);
let logical = if let Some(n) = named {
keyboard::Key::Named(n)
} else {
text.filter(|s| !s.is_empty())
.map(|s| keyboard::Key::Character(SmolStr::new(s))) .map(|s| keyboard::Key::Character(SmolStr::new(s)))
.unwrap_or(keyboard::Key::Unidentified); .unwrap_or(keyboard::Key::Unidentified)
};
let insert_text = if named.is_some() {
None
} else {
text.filter(|s| !s.is_empty()).map(SmolStr::new)
};
if pressed { if pressed {
handle.events.push(Event::Keyboard(keyboard::Event::KeyPressed { handle.events.push(Event::Keyboard(keyboard::Event::KeyPressed {
@ -46,7 +57,7 @@ pub fn push_key_event(
physical_key: physical, physical_key: physical,
location: keyboard::Location::Standard, location: keyboard::Location::Standard,
modifiers, modifiers,
text: text.filter(|s| !s.is_empty()).map(SmolStr::new), text: insert_text,
repeat: false, repeat: false,
})); }));
} else { } else {
@ -60,6 +71,38 @@ pub fn push_key_event(
} }
} }
fn keycode_to_named(keycode: u32) -> Option<keyboard::key::Named> {
use keyboard::key::Named;
match keycode {
36 => Some(Named::Enter),
48 => Some(Named::Tab),
51 => Some(Named::Backspace),
53 => Some(Named::Escape),
117 => Some(Named::Delete),
123 => Some(Named::ArrowLeft),
124 => Some(Named::ArrowRight),
125 => Some(Named::ArrowDown),
126 => Some(Named::ArrowUp),
115 => Some(Named::Home),
119 => Some(Named::End),
116 => Some(Named::PageUp),
121 => Some(Named::PageDown),
122 => Some(Named::F1),
120 => Some(Named::F2),
99 => Some(Named::F3),
118 => Some(Named::F4),
96 => Some(Named::F5),
97 => Some(Named::F6),
98 => Some(Named::F7),
100 => Some(Named::F8),
101 => Some(Named::F9),
109 => Some(Named::F10),
103 => Some(Named::F11),
111 => Some(Named::F12),
_ => None,
}
}
pub fn push_scroll_event( pub fn push_scroll_event(
handle: &mut ViewportHandle, handle: &mut ViewportHandle,
x: f32, x: f32,
@ -67,7 +110,7 @@ pub fn push_scroll_event(
delta_x: f32, delta_x: f32,
delta_y: f32, delta_y: f32,
) { ) {
let position = Point::new(x / handle.scale, y / handle.scale); let position = Point::new(x, y);
handle.cursor = mouse::Cursor::Available(position); handle.cursor = mouse::Cursor::Available(position);
handle.events.push(Event::Mouse(mouse::Event::WheelScrolled { handle.events.push(Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { delta: mouse::ScrollDelta::Pixels {

View File

@ -1,27 +1,50 @@
use iced_wgpu::core::{Color, Element, Length, Theme}; use iced_wgpu::core::text::Wrapping;
use iced_widget::{container, Text}; use iced_wgpu::core::{
Background, Border, Color, Element, Font, Length, Padding, Theme,
};
use iced_widget::text_editor::{self, Style};
#[derive(Debug, Clone)]
pub enum Message {
EditorAction(text_editor::Action),
}
pub struct EditorState { pub struct EditorState {
pub text: String, pub content: text_editor::Content<iced_wgpu::Renderer>,
pub font_size: f32,
} }
impl EditorState { impl EditorState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
text: String::from("Swiftly"), content: text_editor::Content::new(),
font_size: 14.0,
} }
} }
pub fn view(&self) -> Element<'_, (), Theme, iced_wgpu::Renderer> { pub fn update(&mut self, message: Message) {
container( match message {
Text::new(&self.text) Message::EditorAction(action) => {
.size(32) self.content.perform(action);
.color(Color::WHITE), }
) }
.width(Length::Fill) }
pub fn view(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
iced_widget::text_editor(&self.content)
.on_action(Message::EditorAction)
.font(Font::MONOSPACE)
.size(self.font_size)
.height(Length::Fill) .height(Length::Fill)
.center_x(Length::Fill) .padding(Padding { top: 38.0, right: 8.0, bottom: 8.0, left: 8.0 })
.center_y(Length::Fill) .wrapping(Wrapping::Word)
.style(|_theme, _status| Style {
background: Background::Color(Color::from_rgb(0.08, 0.08, 0.10)),
border: Border::default(),
placeholder: Color::from_rgb(0.4, 0.4, 0.4),
value: Color::WHITE,
selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4),
})
.into() .into()
} }
} }

View File

@ -1,17 +1,49 @@
use std::ffi::c_void; use std::ffi::c_void;
use std::ptr::NonNull; use std::ptr::NonNull;
use iced_graphics::{Viewport, Shell}; use iced_graphics::{Shell, Viewport};
use iced_runtime::user_interface::{self, UserInterface}; use iced_runtime::user_interface::{self, UserInterface};
use iced_wgpu::core::renderer::Style; use iced_wgpu::core::renderer::Style;
use iced_wgpu::core::{clipboard, mouse, Color, Font, Pixels, Size, Theme}; use iced_wgpu::core::time::Instant;
use iced_wgpu::core::{clipboard, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme};
use iced_wgpu::Engine; use iced_wgpu::Engine;
use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle}; use raw_window_handle::{
AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle,
};
use crate::editor::EditorState; use crate::editor::{EditorState, Message};
use crate::ViewportHandle; use crate::ViewportHandle;
pub fn create(nsview: *mut c_void, width: f32, height: f32, scale: f32) -> Option<ViewportHandle> { struct MacClipboard;
impl clipboard::Clipboard for MacClipboard {
fn read(&self, _kind: clipboard::Kind) -> Option<String> {
std::process::Command::new("pbpaste")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
}
fn write(&mut self, _kind: clipboard::Kind, contents: String) {
use std::io::Write;
if let Ok(mut child) = std::process::Command::new("pbcopy")
.stdin(std::process::Stdio::piped())
.spawn()
{
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(contents.as_bytes());
}
let _ = child.wait();
}
}
}
pub fn create(
nsview: *mut c_void,
width: f32,
height: f32,
scale: f32,
) -> Option<ViewportHandle> {
let ptr = NonNull::new(nsview)?; let ptr = NonNull::new(nsview)?;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
@ -72,16 +104,17 @@ pub fn create(nsview: *mut c_void, width: f32, height: f32, scale: f32) -> Optio
Shell::headless(), Shell::headless(),
); );
let renderer = iced_wgpu::Renderer::new( let renderer = iced_wgpu::Renderer::new(engine, Font::DEFAULT, Pixels(16.0));
engine,
Font::DEFAULT,
Pixels(16.0),
);
let viewport = Viewport::with_physical_size( let viewport =
Size::new(phys_w.max(1), phys_h.max(1)), Viewport::with_physical_size(Size::new(phys_w.max(1), phys_h.max(1)), scale);
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 { Some(ViewportHandle {
surface, surface,
@ -95,8 +128,8 @@ pub fn create(nsview: *mut c_void, width: f32, height: f32, scale: f32) -> Optio
viewport, viewport,
cache: user_interface::Cache::new(), cache: user_interface::Cache::new(),
state: EditorState::new(), state: EditorState::new(),
events: Vec::new(), events: initial_events,
cursor: mouse::Cursor::Unavailable, cursor: mouse::Cursor::Available(focus_point),
}) })
} }
@ -109,6 +142,10 @@ pub fn render(handle: &mut ViewportHandle) {
let logical_size = handle.viewport.logical_size(); 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 cache = std::mem::take(&mut handle.cache);
let mut ui = UserInterface::build( let mut ui = UserInterface::build(
handle.state.view(), handle.state.view(),
@ -117,8 +154,8 @@ pub fn render(handle: &mut ViewportHandle) {
&mut handle.renderer, &mut handle.renderer,
); );
let mut clipboard = clipboard::Null; let mut clipboard = MacClipboard;
let mut messages: Vec<()> = Vec::new(); let mut messages: Vec<Message> = Vec::new();
let _ = ui.update( let _ = ui.update(
&handle.events, &handle.events,
@ -129,21 +166,31 @@ pub fn render(handle: &mut ViewportHandle) {
); );
handle.events.clear(); handle.events.clear();
let cache = ui.into_cache();
for msg in messages.drain(..) {
handle.state.update(msg);
}
let theme = Theme::Dark; let theme = Theme::Dark;
let style = Style { let style = Style {
text_color: Color::WHITE, text_color: Color::WHITE,
}; };
let mut ui = UserInterface::build(
handle.state.view(),
Size::new(logical_size.width, logical_size.height),
cache,
&mut handle.renderer,
);
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor); ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
handle.cache = ui.into_cache(); handle.cache = ui.into_cache();
let bg = Color::from_rgb(0.08, 0.08, 0.10); let bg = Color::from_rgb(0.08, 0.08, 0.10);
handle.renderer.present( handle
Some(bg), .renderer
handle.format, .present(Some(bg), handle.format, &view, &handle.viewport);
&view,
&handle.viewport,
);
frame.present(); frame.present();
} }
@ -159,10 +206,7 @@ pub fn resize(handle: &mut ViewportHandle, width: f32, height: f32, scale: f32)
handle.height = phys_h; handle.height = phys_h;
handle.scale = scale; handle.scale = scale;
handle.viewport = Viewport::with_physical_size( handle.viewport = Viewport::with_physical_size(Size::new(phys_w, phys_h), scale);
Size::new(phys_w, phys_h),
scale,
);
handle.surface.configure( handle.surface.configure(
&handle.device, &handle.device,