diff --git a/src/AppDelegate.swift b/src/AppDelegate.swift index 570bfc2..82c3f7e 100644 --- a/src/AppDelegate.swift +++ b/src/AppDelegate.swift @@ -6,11 +6,6 @@ import UniformTypeIdentifiers extension Notification.Name { static let focusEditor = Notification.Name("focusEditor") static let focusTitle = Notification.Name("focusTitle") - static let formatDocument = Notification.Name("formatDocument") - static let insertTable = Notification.Name("insertTable") - static let boldSelection = Notification.Name("boldSelection") - static let italicizeSelection = Notification.Name("italicizeSelection") - static let smartEval = Notification.Name("smartEval") } class WindowController { @@ -63,6 +58,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { observeDocumentTitle() observeDocumentText() + syncThemeToViewport() DocumentBrowserController.shared = DocumentBrowserController(appState: appState) @@ -281,19 +277,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { } @objc private func boldSelection() { - NotificationCenter.default.post(name: .boldSelection, object: nil) + viewport?.sendCommand(1) } @objc private func italicizeSelection() { - NotificationCenter.default.post(name: .italicizeSelection, object: nil) + viewport?.sendCommand(2) } @objc private func insertTable() { - NotificationCenter.default.post(name: .insertTable, object: nil) + viewport?.sendCommand(3) } @objc private func smartEval() { - NotificationCenter.default.post(name: .smartEval, object: nil) + viewport?.sendCommand(4) } @objc private func openNote() { @@ -412,7 +408,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { }() @objc private func formatDocument() { - NotificationCenter.default.post(name: .formatDocument, object: nil) + viewport?.sendCommand(10) } @objc private func openSettings() { @@ -421,9 +417,24 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc private func settingsDidChange() { window.backgroundColor = Theme.current.base + syncThemeToViewport() window.contentView?.needsDisplay = true } + private func syncThemeToViewport() { + let mode = ConfigManager.shared.themeMode + let name: String + switch mode { + case "dark": name = "mocha" + case "light": name = "latte" + default: + let appearance = NSApp.effectiveAppearance + let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua + name = isDark ? "mocha" : "latte" + } + viewport?.setTheme(name) + } + @objc private func toggleBrowser() { DocumentBrowserController.shared?.toggle() } diff --git a/src/IcedViewportView.swift b/src/IcedViewportView.swift index 5703435..517886c 100644 --- a/src/IcedViewportView.swift +++ b/src/IcedViewportView.swift @@ -159,7 +159,7 @@ class IcedViewportView: NSView { if cmd && !shift { switch chars { - case "a", "b", "c", "i", "v", "x", "z", "p", "t", + case "a", "b", "c", "e", "f", "g", "i", "v", "x", "z", "p", "t", "=", "+", "-", "0": keyDown(with: event) return true @@ -214,4 +214,16 @@ class IcedViewportView: NSView { viewport_free_string(cstr) return result } + + func sendCommand(_ command: UInt32) { + guard let h = viewportHandle else { return } + viewport_send_command(h, command) + } + + func setTheme(_ name: String) { + guard let h = viewportHandle else { return } + name.withCString { cstr in + viewport_set_theme(h, cstr) + } + } } diff --git a/viewport/include/acord.h b/viewport/include/acord.h index 083919c..fc19417 100644 --- a/viewport/include/acord.h +++ b/viewport/include/acord.h @@ -53,4 +53,8 @@ char *viewport_get_text(struct ViewportHandle *handle); void viewport_free_string(char *s); +void viewport_set_theme(struct ViewportHandle *_handle, const char *name); + +void viewport_send_command(struct ViewportHandle *handle, uint32_t command); + #endif /* ACORD_VIEWPORT_H */ diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index dc3ce60..7af85b8 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -13,6 +13,7 @@ use iced_widget::container; use iced_widget::markdown; use iced_widget::text_editor::{self, Action, Binding, Cursor, KeyPress, Motion, Position, Status, Style}; use iced_wgpu::core::text::highlighter::Format; +use crate::palette; use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings}; #[derive(Debug, Clone)] @@ -48,17 +49,18 @@ pub struct EditorState { } fn md_style() -> markdown::Style { + let p = palette::current(); markdown::Style { font: Font::default(), inline_code_highlight: Highlight { - background: Color::from_rgb(0.188, 0.188, 0.259).into(), + background: p.surface0.into(), border: border::rounded(4), }, inline_code_padding: padding::left(2).right(2), - inline_code_color: Color::from_rgb(0.651, 0.890, 0.631), + inline_code_color: p.green, inline_code_font: Font::MONOSPACE, code_block_font: Font::MONOSPACE, - link_color: Color::from_rgb(0.537, 0.706, 0.980), + link_color: p.blue, } } @@ -402,12 +404,15 @@ impl EditorState { ) .width(Length::Fill) .height(Length::Fill) - .style(|_theme: &Theme| container::Style { - background: Some(Background::Color(Color::from_rgb(0.08, 0.08, 0.10))), - border: Border::default(), - text_color: Some(Color::from_rgb(0.804, 0.839, 0.957)), - shadow: Shadow::default(), - snap: false, + .style(|_theme: &Theme| { + let p = palette::current(); + container::Style { + background: Some(Background::Color(p.base)), + border: Border::default(), + text_color: Some(p.text), + shadow: Shadow::default(), + snap: false, + } }) .into() } else { @@ -420,12 +425,15 @@ impl EditorState { .padding(Padding { top: top_pad, right: 8.0, bottom: 8.0, left: 8.0 }) .wrapping(Wrapping::Word) .key_binding(macos_key_binding) - .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), + .style(|_theme, _status| { + let p = palette::current(); + Style { + background: Background::Color(p.base), + border: Border::default(), + placeholder: p.overlay0, + value: p.text, + selection: Color { a: 0.4, ..p.blue }, + } }); let settings = SyntaxSettings { @@ -478,18 +486,21 @@ impl EditorState { iced_widget::text(format!("{mode_label} Ln {line}, Col {col}")) .font(Font::MONOSPACE) .size(11.0) - .color(Color::from_rgb(0.55, 0.55, 0.55)) + .color(palette::current().overlay1) .into(), ]) ) .width(Length::Fill) .padding(Padding { top: 3.0, right: 10.0, bottom: 3.0, left: 10.0 }) - .style(|_theme: &Theme| container::Style { - background: Some(Background::Color(Color::from_rgb(0.12, 0.12, 0.14))), - border: Border::default(), - text_color: None, - shadow: Shadow::default(), - snap: false, + .style(|_theme: &Theme| { + let p = palette::current(); + container::Style { + background: Some(Background::Color(p.mantle)), + border: Border::default(), + text_color: None, + shadow: Shadow::default(), + snap: false, + } }); let mut col_items: Vec> = @@ -539,7 +550,7 @@ impl canvas::Program for Gutter { frame.fill_rectangle( Point::ORIGIN, bounds.size(), - Color::from_rgb(0.06, 0.06, 0.08), + palette::current().crust, ); let first_visible = (self.scroll_offset / lh).floor() as usize; @@ -572,10 +583,11 @@ impl canvas::Program for Gutter { continue; } source_num += 1; + let p = palette::current(); let color = if line_idx == self.cursor_line { - Color::from_rgb(0.55, 0.55, 0.62) + p.overlay1 } else { - Color::from_rgb(0.35, 0.35, 0.42) + p.surface2 }; frame.fill_text(canvas::Text { content: format!("{}", source_num), diff --git a/viewport/src/handle.rs b/viewport/src/handle.rs index f697945..1d4d4ed 100644 --- a/viewport/src/handle.rs +++ b/viewport/src/handle.rs @@ -12,6 +12,7 @@ use raw_window_handle::{ }; use crate::editor::{EditorState, Message}; +use crate::palette; use crate::ViewportHandle; struct MacClipboard; @@ -209,10 +210,9 @@ pub fn render(handle: &mut ViewportHandle) { ui.draw(&mut handle.renderer, &theme, &style, handle.cursor); handle.cache = ui.into_cache(); - let bg = Color::from_rgb(0.08, 0.08, 0.10); handle .renderer - .present(Some(bg), handle.format, &view, &handle.viewport); + .present(Some(palette::current().base), handle.format, &view, &handle.viewport); frame.present(); } diff --git a/viewport/src/lib.rs b/viewport/src/lib.rs index 7a7cbe4..556ab60 100644 --- a/viewport/src/lib.rs +++ b/viewport/src/lib.rs @@ -3,6 +3,7 @@ use std::ffi::{c_char, c_void, CStr, CString}; mod bridge; mod editor; mod handle; +pub mod palette; mod syntax; pub use acord_core::*; @@ -172,3 +173,34 @@ pub extern "C" fn viewport_free_string(s: *mut c_char) { if s.is_null() { return; } unsafe { drop(CString::from_raw(s)); } } + +#[no_mangle] +pub extern "C" fn viewport_set_theme(_handle: *mut ViewportHandle, name: *const c_char) { + let s = if name.is_null() { + "mocha" + } else { + unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("mocha") + }; + palette::set_theme(s); +} + +#[no_mangle] +pub extern "C" fn viewport_send_command(handle: *mut ViewportHandle, command: u32) { + let h = match unsafe { handle.as_mut() } { + Some(h) => h, + None => return, + }; + let msg = match command { + 1 => editor::Message::ToggleBold, + 2 => editor::Message::ToggleItalic, + 3 => editor::Message::InsertTable, + 4 => editor::Message::SmartEval, + 5 => editor::Message::Evaluate, + 6 => editor::Message::TogglePreview, + 7 => editor::Message::ZoomIn, + 8 => editor::Message::ZoomOut, + 9 => editor::Message::ZoomReset, + _ => return, + }; + h.state.update(msg); +} diff --git a/viewport/src/palette.rs b/viewport/src/palette.rs new file mode 100644 index 0000000..9beb205 --- /dev/null +++ b/viewport/src/palette.rs @@ -0,0 +1,106 @@ +use iced_wgpu::core::Color; +use std::cell::RefCell; + +#[derive(Clone, Copy)] +pub struct Palette { + pub rosewater: Color, + pub flamingo: Color, + pub pink: Color, + pub mauve: Color, + pub red: Color, + pub maroon: Color, + pub peach: Color, + pub yellow: Color, + pub green: Color, + pub teal: Color, + pub sky: Color, + pub sapphire: Color, + pub blue: Color, + pub lavender: Color, + pub text: Color, + pub subtext1: Color, + pub subtext0: Color, + pub overlay2: Color, + pub overlay1: Color, + pub overlay0: Color, + pub surface2: Color, + pub surface1: Color, + pub surface0: Color, + pub base: Color, + pub mantle: Color, + pub crust: Color, +} + +pub static MOCHA: Palette = Palette { + rosewater: Color::from_rgb(0.961, 0.878, 0.863), + flamingo: Color::from_rgb(0.949, 0.804, 0.804), + pink: Color::from_rgb(0.961, 0.761, 0.906), + mauve: Color::from_rgb(0.796, 0.651, 0.969), + red: Color::from_rgb(0.953, 0.545, 0.659), + maroon: Color::from_rgb(0.922, 0.627, 0.675), + peach: Color::from_rgb(0.980, 0.702, 0.529), + yellow: Color::from_rgb(0.976, 0.886, 0.686), + green: Color::from_rgb(0.651, 0.890, 0.631), + teal: Color::from_rgb(0.580, 0.886, 0.835), + sky: Color::from_rgb(0.537, 0.863, 0.922), + sapphire: Color::from_rgb(0.455, 0.780, 0.925), + blue: Color::from_rgb(0.537, 0.706, 0.980), + lavender: Color::from_rgb(0.706, 0.745, 0.996), + text: Color::from_rgb(0.804, 0.839, 0.957), + subtext1: Color::from_rgb(0.729, 0.761, 0.871), + subtext0: Color::from_rgb(0.651, 0.678, 0.784), + overlay2: Color::from_rgb(0.576, 0.600, 0.698), + overlay1: Color::from_rgb(0.498, 0.518, 0.612), + overlay0: Color::from_rgb(0.424, 0.439, 0.525), + surface2: Color::from_rgb(0.345, 0.357, 0.439), + surface1: Color::from_rgb(0.271, 0.278, 0.353), + surface0: Color::from_rgb(0.192, 0.196, 0.267), + base: Color::from_rgb(0.118, 0.118, 0.180), + mantle: Color::from_rgb(0.094, 0.094, 0.145), + crust: Color::from_rgb(0.067, 0.067, 0.106), +}; + +pub static LATTE: Palette = Palette { + rosewater: Color::from_rgb(0.863, 0.541, 0.471), + flamingo: Color::from_rgb(0.867, 0.471, 0.471), + pink: Color::from_rgb(0.918, 0.463, 0.796), + mauve: Color::from_rgb(0.533, 0.224, 0.937), + red: Color::from_rgb(0.824, 0.059, 0.224), + maroon: Color::from_rgb(0.902, 0.271, 0.325), + peach: Color::from_rgb(0.996, 0.392, 0.043), + yellow: Color::from_rgb(0.875, 0.557, 0.114), + green: Color::from_rgb(0.251, 0.627, 0.169), + teal: Color::from_rgb(0.090, 0.573, 0.600), + sky: Color::from_rgb(0.016, 0.647, 0.898), + sapphire: Color::from_rgb(0.125, 0.624, 0.710), + blue: Color::from_rgb(0.118, 0.400, 0.961), + lavender: Color::from_rgb(0.447, 0.529, 0.992), + text: Color::from_rgb(0.298, 0.310, 0.412), + subtext1: Color::from_rgb(0.361, 0.373, 0.467), + subtext0: Color::from_rgb(0.424, 0.435, 0.522), + overlay2: Color::from_rgb(0.486, 0.498, 0.576), + overlay1: Color::from_rgb(0.549, 0.561, 0.631), + overlay0: Color::from_rgb(0.612, 0.627, 0.690), + surface2: Color::from_rgb(0.675, 0.690, 0.745), + surface1: Color::from_rgb(0.737, 0.753, 0.800), + surface0: Color::from_rgb(0.800, 0.816, 0.855), + base: Color::from_rgb(0.937, 0.945, 0.961), + mantle: Color::from_rgb(0.902, 0.914, 0.937), + crust: Color::from_rgb(0.863, 0.878, 0.910), +}; + +thread_local! { + static CURRENT: RefCell<&'static Palette> = const { RefCell::new(&MOCHA) }; +} + +pub fn current() -> &'static Palette { + CURRENT.with(|c| *c.borrow()) +} + +pub fn set_theme(name: &str) { + let pal = match name { + "latte" | "light" => &LATTE, + _ => &MOCHA, + }; + CURRENT.with(|c| *c.borrow_mut() = pal); +} diff --git a/viewport/src/syntax.rs b/viewport/src/syntax.rs index dd18345..494501e 100644 --- a/viewport/src/syntax.rs +++ b/viewport/src/syntax.rs @@ -4,6 +4,7 @@ use iced_wgpu::core::text::highlighter; use iced_wgpu::core::Color; use acord_core::highlight::{highlight_source, HighlightSpan}; use crate::editor::{RESULT_PREFIX, ERROR_PREFIX}; +use crate::palette; pub const EVAL_RESULT_KIND: u8 = 24; pub const EVAL_ERROR_KIND: u8 = 25; @@ -107,33 +108,34 @@ impl highlighter::Highlighter for SyntaxHighlighter { } pub fn highlight_color(kind: u8) -> Color { + let p = palette::current(); match kind { - 0 => Color::from_rgb(0.804, 0.569, 0.945), // keyword - mauve - 1 => Color::from_rgb(0.537, 0.706, 0.980), // function - blue - 2 => Color::from_rgb(0.604, 0.831, 0.898), // function.builtin - teal - 3 => Color::from_rgb(0.976, 0.827, 0.522), // type - yellow - 4 => Color::from_rgb(0.976, 0.827, 0.522), // type.builtin - yellow - 5 => Color::from_rgb(0.569, 0.878, 0.800), // constructor - teal - 6 => Color::from_rgb(0.988, 0.702, 0.529), // constant - peach - 7 => Color::from_rgb(0.988, 0.702, 0.529), // constant.builtin - peach - 8 => Color::from_rgb(0.651, 0.890, 0.631), // string - green - 9 => Color::from_rgb(0.988, 0.702, 0.529), // number - peach - 10 => Color::from_rgb(0.424, 0.443, 0.537), // comment - overlay0 - 11 => Color::from_rgb(0.804, 0.839, 0.957), // variable - text - 12 => Color::from_rgb(0.949, 0.604, 0.584), // variable.builtin - red - 13 => Color::from_rgb(0.949, 0.773, 0.584), // variable.parameter - flamingo - 14 => Color::from_rgb(0.604, 0.831, 0.898), // operator - sky - 15 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation - overlay2 - 16 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.bracket - overlay2 - 17 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.delimiter - overlay2 - 18 => Color::from_rgb(0.537, 0.706, 0.980), // property - blue - 19 => Color::from_rgb(0.804, 0.569, 0.945), // tag - mauve - 20 => Color::from_rgb(0.976, 0.827, 0.522), // attribute - yellow - 21 => Color::from_rgb(0.569, 0.878, 0.800), // label - teal - 22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red - 23 => Color::from_rgb(0.804, 0.839, 0.957), // embedded - text - 24 => Color::from_rgb(0.651, 0.890, 0.631), // eval result - green - 25 => Color::from_rgb(0.890, 0.400, 0.400), // eval error - muted red - _ => Color::from_rgb(0.804, 0.839, 0.957), // default text + 0 => p.mauve, + 1 => p.blue, + 2 => p.teal, + 3 => p.yellow, + 4 => p.yellow, + 5 => p.teal, + 6 => p.peach, + 7 => p.peach, + 8 => p.green, + 9 => p.peach, + 10 => p.overlay0, + 11 => p.text, + 12 => p.red, + 13 => p.flamingo, + 14 => p.sky, + 15 => p.overlay2, + 16 => p.overlay2, + 17 => p.overlay2, + 18 => p.blue, + 19 => p.mauve, + 20 => p.yellow, + 21 => p.teal, + 22 => p.red, + 23 => p.text, + 24 => p.green, + 25 => p.maroon, + _ => p.text, } }