forked from jess/Acord
1
0
Fork 0

Windows init window wrapper and menus, polling.

This commit is contained in:
jess 2026-04-17 13:09:52 -07:00
parent 17d513c62f
commit d746c9abd6
7 changed files with 572 additions and 3 deletions

View File

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["core", "viewport"] members = ["core", "viewport", "windows"]
resolver = "2" resolver = "2"
[profile.release] [profile.release]

View File

@ -39,8 +39,8 @@ pub struct ViewportHandle {
renderer: iced_wgpu::Renderer, renderer: iced_wgpu::Renderer,
viewport: Viewport, viewport: Viewport,
cache: user_interface::Cache, cache: user_interface::Cache,
state: EditorState, pub state: EditorState,
events: Vec<Event>, pub events: Vec<Event>,
cursor: iced_wgpu::core::mouse::Cursor, cursor: iced_wgpu::core::mouse::Cursor,
/// Set true on any FFI input or state-change call. handle::render() early-returns /// Set true on any FFI input or state-change call. handle::render() early-returns
/// when this is false AND no pending eval debounce, so the vsync display link /// when this is false AND no pending eval debounce, so the vsync display link

28
windows/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "acord-windows"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "acord"
path = "src/main.rs"
[dependencies]
acord-core = { path = "../core" }
acord-viewport = { path = "../viewport" }
winit = "0.30"
muda = "0.16"
arboard = "3"
rfd = "0.15"
raw-window-handle = "0.6"
wgpu = "27"
iced_wgpu = "0.14"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "6"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"

364
windows/src/app.rs Normal file
View File

@ -0,0 +1,364 @@
use std::ffi::{c_void, CString};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, PhysicalPosition};
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::ActiveEventLoop;
use winit::keyboard::{Key, NamedKey, ModifiersState};
use winit::window::{Window, WindowAttributes, WindowId};
use acord_viewport::{
viewport_create, viewport_destroy, viewport_render, viewport_resize,
viewport_set_text, viewport_get_text, viewport_set_theme, viewport_set_lang,
viewport_set_line_indicator, viewport_set_gutter_rainbow,
viewport_send_command, viewport_free_string, viewport_render_mode,
viewport_export_crate, ViewportHandle,
};
use crate::config::Config;
use crate::menu::{AppMenu, MenuAction};
pub struct App {
window: Option<Window>,
handle: *mut ViewportHandle,
config: Config,
menu: Option<AppMenu>,
cursor_pos: PhysicalPosition<f64>,
scale: f32,
}
impl App {
pub fn new() -> Self {
Self {
window: None,
handle: std::ptr::null_mut(),
config: Config::load(),
menu: None,
cursor_pos: PhysicalPosition::new(0.0, 0.0),
scale: 1.0,
}
}
fn sync_settings(&self) {
if self.handle.is_null() { return; }
let theme = match self.config.theme_mode() {
"dark" => "kicad",
"light" => "latte",
_ => "kicad", // Windows: default dark. No NSAppearance auto-detect.
};
let c_theme = CString::new(theme).unwrap();
viewport_set_theme(self.handle, c_theme.as_ptr());
let ind = CString::new(self.config.line_indicator()).unwrap();
viewport_set_line_indicator(self.handle, ind.as_ptr());
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
}
fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) {
if self.handle.is_null() { return; }
match action {
MenuAction::Quit => event_loop.exit(),
MenuAction::Bold => { viewport_send_command(self.handle, 1); }
MenuAction::Italic => { viewport_send_command(self.handle, 2); }
MenuAction::InsertTable => { viewport_send_command(self.handle, 3); }
MenuAction::Evaluate => { viewport_send_command(self.handle, 5); }
MenuAction::ZoomIn => { viewport_send_command(self.handle, 7); }
MenuAction::ZoomOut => { viewport_send_command(self.handle, 8); }
MenuAction::ZoomReset => { viewport_send_command(self.handle, 9); }
MenuAction::LiveMode => { viewport_send_command(self.handle, 11); }
MenuAction::EditorMode => { viewport_send_command(self.handle, 12); }
MenuAction::ViewMode => { viewport_send_command(self.handle, 13); }
MenuAction::Find => { viewport_send_command(self.handle, 14); }
MenuAction::Open => self.open_file(),
MenuAction::Save => self.save_file(),
MenuAction::SaveAs => self.save_file_as(),
MenuAction::NewNote => self.new_note(),
MenuAction::Undo => { /* TODO */ },
MenuAction::Redo => { /* TODO */ },
MenuAction::ExportCrate => { /* TODO */ },
}
}
fn open_file(&mut self) {
let dialog = rfd::FileDialog::new()
.add_filter("Markdown", &["md", "markdown"])
.add_filter("All Files", &["*"]);
if let Some(path) = dialog.pick_file() {
if let Ok(text) = std::fs::read_to_string(&path) {
let c = CString::new(text).unwrap_or_default();
viewport_set_text(self.handle, c.as_ptr());
let ext = path.extension()
.and_then(|e| e.to_str())
.unwrap_or("md");
let c_ext = CString::new(ext).unwrap();
viewport_set_lang(self.handle, c_ext.as_ptr());
if let Some(w) = &self.window {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("Acord");
w.set_title(&format!("{name} - Acord"));
}
}
}
}
fn save_file(&self) {
self.save_file_as();
}
fn save_file_as(&self) {
let dialog = rfd::FileDialog::new()
.add_filter("Markdown", &["md"])
.add_filter("All Files", &["*"])
.set_file_name("note.md");
if let Some(path) = dialog.save_file() {
let text_ptr = viewport_get_text(self.handle);
if !text_ptr.is_null() {
let text = unsafe { std::ffi::CStr::from_ptr(text_ptr) }
.to_string_lossy()
.into_owned();
viewport_free_string(text_ptr);
let _ = std::fs::write(&path, text);
}
}
}
fn new_note(&mut self) {
let empty = CString::new("").unwrap();
viewport_set_text(self.handle, empty.as_ptr());
if let Some(w) = &self.window {
w.set_title("Acord");
}
}
fn winit_button(button: MouseButton) -> u8 {
match button {
MouseButton::Left => 0,
MouseButton::Right => 1,
MouseButton::Middle => 2,
MouseButton::Other(n) => n as u8,
_ => 0,
}
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() { return; }
let attrs = WindowAttributes::default()
.with_title("Acord")
.with_inner_size(LogicalSize::new(1100.0, 750.0));
let window = event_loop.create_window(attrs).expect("create window");
self.scale = window.scale_factor() as f32;
let size = window.inner_size();
let w = size.width as f32 / self.scale;
let h = size.height as f32 / self.scale;
// Get raw HWND and pass to viewport.
use raw_window_handle::HasWindowHandle;
let wh = window.window_handle().expect("window handle");
let raw = wh.as_raw();
let hwnd = match raw {
#[cfg(target_os = "windows")]
raw_window_handle::RawWindowHandle::Win32(h) => h.hwnd.get() as *mut c_void,
#[cfg(target_os = "macos")]
raw_window_handle::RawWindowHandle::AppKit(h) => h.ns_view.as_ptr(),
_ => std::ptr::null_mut(),
};
self.handle = viewport_create(hwnd, w, h, self.scale);
self.sync_settings();
// Set up native menu bar.
let app_menu = AppMenu::new();
#[cfg(target_os = "windows")]
{
use raw_window_handle::HasWindowHandle;
if let Ok(wh) = window.window_handle() {
if let raw_window_handle::RawWindowHandle::Win32(h) = wh.as_raw() {
use winapi::shared::windef::HWND;
app_menu.menu.init_for_hwnd(h.hwnd.get() as HWND).ok();
}
}
}
self.menu = Some(app_menu);
self.window = Some(window);
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
if self.handle.is_null() { return; }
match event {
WindowEvent::CloseRequested => {
if !self.handle.is_null() {
viewport_destroy(self.handle);
self.handle = std::ptr::null_mut();
}
event_loop.exit();
}
WindowEvent::Resized(size) => {
let w = size.width as f32 / self.scale;
let h = size.height as f32 / self.scale;
viewport_resize(self.handle, w, h, self.scale);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.scale = scale_factor as f32;
if let Some(win) = &self.window {
let size = win.inner_size();
let w = size.width as f32 / self.scale;
let h = size.height as f32 / self.scale;
viewport_resize(self.handle, w, h, self.scale);
}
}
WindowEvent::RedrawRequested => {
viewport_render(self.handle);
}
WindowEvent::CursorMoved { position, .. } => {
self.cursor_pos = position;
let x = position.x as f32 / self.scale;
let y = position.y as f32 / self.scale;
acord_viewport::viewport_mouse_event(
self.handle, x, y, 255, false,
);
}
WindowEvent::MouseInput { state, button, .. } => {
let x = self.cursor_pos.x as f32 / self.scale;
let y = self.cursor_pos.y as f32 / self.scale;
let pressed = state == ElementState::Pressed;
acord_viewport::viewport_mouse_event(
self.handle, x, y, Self::winit_button(button), pressed,
);
}
WindowEvent::MouseWheel { delta, .. } => {
let x = self.cursor_pos.x as f32 / self.scale;
let y = self.cursor_pos.y as f32 / self.scale;
let (dx, dy) = match delta {
MouseScrollDelta::LineDelta(dx, dy) => (dx * 20.0, dy * 20.0),
MouseScrollDelta::PixelDelta(d) => (d.x as f32, d.y as f32),
};
acord_viewport::viewport_scroll_event(self.handle, x, y, dx, -dy);
}
WindowEvent::KeyboardInput { event, .. } => {
let pressed = event.state == ElementState::Pressed;
let text_str = event.text.as_ref().map(|s| s.to_string());
let text_c = text_str.as_deref()
.and_then(|s| CString::new(s).ok());
let text_ptr = text_c.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null());
let keycode = winit_key_to_code(&event.logical_key);
let modifiers = if let Some(w) = &self.window {
// No direct modifier query on winit 0.30 Window.
// Modifiers come via ModifiersChanged. We track them.
0u32
} else {
0u32
};
acord_viewport::viewport_key_event(
self.handle, keycode, modifiers, pressed, text_ptr,
);
}
WindowEvent::ModifiersChanged(mods) => {
if !self.handle.is_null() {
let state = mods.state();
let h = unsafe { &mut *self.handle };
use iced_wgpu::core::keyboard;
use iced_wgpu::core::Event;
h.events.push(Event::Keyboard(
keyboard::Event::ModifiersChanged(decode_winit_modifiers(state)),
));
h.needs_redraw = true;
}
}
_ => {}
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// Poll menu events.
while let Some(action) = AppMenu::poll() {
self.dispatch_menu(action, _event_loop);
}
// Request a redraw if the viewport has pending work.
if let Some(w) = &self.window {
if !self.handle.is_null() {
// Always request redraw — viewport_render short-circuits
// internally when idle (needs_redraw == false && no pending
// eval). Requesting unconditionally is simpler than reading
// the handle's state from here, and wgpu PresentMode::Fifo
// throttles to vsync anyway.
w.request_redraw();
}
}
}
}
/// Map winit logical keys to the macOS-style keycodes the bridge expects.
/// For Named keys, return the matching keycode. For character keys, the
/// bridge ignores the keycode and uses the text parameter directly, so
/// we return 0 (unmapped).
fn winit_key_to_code(key: &Key) -> u32 {
match key {
Key::Named(n) => match n {
NamedKey::Enter => 36,
NamedKey::Tab => 48,
NamedKey::Backspace => 51,
NamedKey::Escape => 53,
NamedKey::Delete => 117,
NamedKey::ArrowLeft => 123,
NamedKey::ArrowRight => 124,
NamedKey::ArrowDown => 125,
NamedKey::ArrowUp => 126,
NamedKey::Home => 115,
NamedKey::End => 119,
NamedKey::PageUp => 116,
NamedKey::PageDown => 121,
NamedKey::F1 => 122,
NamedKey::F2 => 120,
NamedKey::F3 => 99,
NamedKey::F4 => 118,
NamedKey::F5 => 96,
NamedKey::F6 => 97,
NamedKey::F7 => 98,
NamedKey::F8 => 100,
NamedKey::F9 => 101,
NamedKey::F10 => 109,
NamedKey::F11 => 103,
NamedKey::F12 => 111,
_ => 0,
},
_ => 0,
}
}
fn encode_modifiers(state: ModifiersState) -> u32 {
let mut flags = 0u32;
if state.shift_key() { flags |= 1 << 17; }
if state.control_key() { flags |= 1 << 18; }
if state.alt_key() { flags |= 1 << 19; }
if state.super_key() { flags |= 1 << 20; }
flags
}
fn decode_winit_modifiers(state: ModifiersState) -> iced_wgpu::core::keyboard::Modifiers {
let mut m = iced_wgpu::core::keyboard::Modifiers::empty();
if state.shift_key() { m |= iced_wgpu::core::keyboard::Modifiers::SHIFT; }
if state.control_key() { m |= iced_wgpu::core::keyboard::Modifiers::CTRL; }
if state.alt_key() { m |= iced_wgpu::core::keyboard::Modifiers::ALT; }
// On Windows, Ctrl is the action modifier (not Logo/Super).
// Map Ctrl to LOGO so iced's Cmd+C/V/X bindings work via Ctrl on Windows.
if state.control_key() { m |= iced_wgpu::core::keyboard::Modifiers::LOGO; }
m
}

62
windows/src/config.rs Normal file
View File

@ -0,0 +1,62 @@
use std::collections::HashMap;
use std::path::PathBuf;
pub struct Config {
path: PathBuf,
data: HashMap<String, String>,
}
impl Config {
pub fn load() -> Self {
let dir = config_dir();
std::fs::create_dir_all(&dir).ok();
let notes = dir.join("notes");
std::fs::create_dir_all(&notes).ok();
let path = dir.join("config.json");
let data = std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
Self { path, data }
}
pub fn save(&self) {
if let Ok(json) = serde_json::to_string_pretty(&self.data) {
let _ = std::fs::write(&self.path, json);
}
}
pub fn get(&self, key: &str, default: &str) -> String {
self.data.get(key).cloned().unwrap_or_else(|| default.to_string())
}
pub fn set(&mut self, key: &str, value: &str) {
self.data.insert(key.to_string(), value.to_string());
self.save();
}
pub fn theme_mode(&self) -> &str {
self.data.get("themeMode").map(|s| s.as_str()).unwrap_or("auto")
}
pub fn line_indicator(&self) -> &str {
self.data.get("lineIndicatorMode").map(|s| s.as_str()).unwrap_or("on")
}
pub fn gutter_rainbow(&self) -> bool {
self.data.get("gutterRainbow").map(|s| s != "false").unwrap_or(true)
}
pub fn notes_dir(&self) -> PathBuf {
self.data.get("autoSaveDirectory")
.map(PathBuf::from)
.unwrap_or_else(|| config_dir().join("notes"))
}
}
fn config_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".acord")
}

9
windows/src/main.rs Normal file
View File

@ -0,0 +1,9 @@
mod app;
mod config;
mod menu;
fn main() {
let event_loop = winit::event_loop::EventLoop::new().expect("event loop");
let mut state = app::App::new();
event_loop.run_app(&mut state).expect("run");
}

106
windows/src/menu.rs Normal file
View File

@ -0,0 +1,106 @@
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu, accelerator::Accelerator};
use muda::accelerator::{Code, Modifiers};
pub struct AppMenu {
pub menu: Menu,
}
pub enum MenuAction {
NewNote,
Open,
Save,
SaveAs,
Quit,
Undo,
Redo,
Bold,
Italic,
InsertTable,
Evaluate,
LiveMode,
EditorMode,
ViewMode,
ZoomIn,
ZoomOut,
ZoomReset,
Find,
ExportCrate,
}
impl AppMenu {
pub fn new() -> Self {
let menu = Menu::new();
let file = Submenu::new("File", true);
file.append(&MenuItem::with_id("new", "New Note", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyN)))).ok();
file.append(&MenuItem::with_id("open", "Open...", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyO)))).ok();
file.append(&PredefinedMenuItem::separator()).ok();
file.append(&MenuItem::with_id("save", "Save", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyS)))).ok();
file.append(&MenuItem::with_id("save_as", "Save As...", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyS)))).ok();
file.append(&PredefinedMenuItem::separator()).ok();
file.append(&MenuItem::with_id("export_crate", "Export as Rust Library", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyE)))).ok();
file.append(&PredefinedMenuItem::separator()).ok();
file.append(&MenuItem::with_id("quit", "Quit", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyQ)))).ok();
let edit = Submenu::new("Edit", true);
edit.append(&MenuItem::with_id("undo", "Undo", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyZ)))).ok();
edit.append(&MenuItem::with_id("redo", "Redo", true, Some(Accelerator::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyZ)))).ok();
edit.append(&PredefinedMenuItem::separator()).ok();
edit.append(&PredefinedMenuItem::cut(None)).ok();
edit.append(&PredefinedMenuItem::copy(None)).ok();
edit.append(&PredefinedMenuItem::paste(None)).ok();
edit.append(&PredefinedMenuItem::select_all(None)).ok();
edit.append(&PredefinedMenuItem::separator()).ok();
edit.append(&MenuItem::with_id("find", "Find...", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyF)))).ok();
edit.append(&PredefinedMenuItem::separator()).ok();
edit.append(&MenuItem::with_id("bold", "Bold", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyB)))).ok();
edit.append(&MenuItem::with_id("italic", "Italic", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyI)))).ok();
edit.append(&MenuItem::with_id("table", "Insert Table", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyT)))).ok();
let render = Submenu::new("Render", true);
render.append(&MenuItem::with_id("live", "Live", true, None)).ok();
render.append(&MenuItem::with_id("editor", "Editor", true, None)).ok();
render.append(&MenuItem::with_id("view", "View", true, None)).ok();
render.append(&PredefinedMenuItem::separator()).ok();
render.append(&MenuItem::with_id("eval", "Evaluate", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Enter)))).ok();
let view = Submenu::new("View", true);
view.append(&MenuItem::with_id("zoom_in", "Zoom In", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Equal)))).ok();
view.append(&MenuItem::with_id("zoom_out", "Zoom Out", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Minus)))).ok();
view.append(&MenuItem::with_id("zoom_reset", "Reset Zoom", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Digit0)))).ok();
menu.append(&file).ok();
menu.append(&edit).ok();
menu.append(&render).ok();
menu.append(&view).ok();
Self { menu }
}
pub fn poll() -> Option<MenuAction> {
MenuEvent::receiver().try_recv().ok().and_then(|e| {
match e.id().0.as_str() {
"new" => Some(MenuAction::NewNote),
"open" => Some(MenuAction::Open),
"save" => Some(MenuAction::Save),
"save_as" => Some(MenuAction::SaveAs),
"quit" => Some(MenuAction::Quit),
"undo" => Some(MenuAction::Undo),
"redo" => Some(MenuAction::Redo),
"bold" => Some(MenuAction::Bold),
"italic" => Some(MenuAction::Italic),
"table" => Some(MenuAction::InsertTable),
"eval" => Some(MenuAction::Evaluate),
"live" => Some(MenuAction::LiveMode),
"editor" => Some(MenuAction::EditorMode),
"view" => Some(MenuAction::ViewMode),
"zoom_in" => Some(MenuAction::ZoomIn),
"zoom_out" => Some(MenuAction::ZoomOut),
"zoom_reset" => Some(MenuAction::ZoomReset),
"find" => Some(MenuAction::Find),
"export_crate" => Some(MenuAction::ExportCrate),
_ => None,
}
})
}
}