diff --git a/viewport/Cargo.toml b/viewport/Cargo.toml index 1da93ba..d673648 100644 --- a/viewport/Cargo.toml +++ b/viewport/Cargo.toml @@ -11,7 +11,7 @@ swiftly-core = { path = "../core" } iced_wgpu = "0.14" iced_graphics = "0.14" iced_runtime = "0.14" -iced_widget = { version = "0.14", features = ["wgpu"] } +iced_widget = { version = "0.14", features = ["wgpu", "markdown"] } wgpu = "27" raw-window-handle = "0.6" pollster = "0.4" diff --git a/viewport/src/bridge.rs b/viewport/src/bridge.rs index bcf227d..2168b80 100644 --- a/viewport/src/bridge.rs +++ b/viewport/src/bridge.rs @@ -44,7 +44,8 @@ pub fn push_key_event( .unwrap_or(keyboard::Key::Unidentified) }; - let insert_text = if named.is_some() { + let has_action_modifier = modifiers.logo() || modifiers.control(); + let insert_text = if named.is_some() || has_action_modifier { None } else { text.filter(|s| !s.is_empty()).map(SmolStr::new) diff --git a/viewport/src/editor.rs b/viewport/src/editor.rs index 9d875a1..28aa589 100644 --- a/viewport/src/editor.rs +++ b/viewport/src/editor.rs @@ -1,20 +1,43 @@ use iced_wgpu::core::keyboard; use iced_wgpu::core::keyboard::key; -use iced_wgpu::core::text::Wrapping; +use iced_wgpu::core::text::{Highlight, Wrapping}; use iced_wgpu::core::{ - Background, Border, Color, Element, Font, Length, Padding, Shadow, Theme, + border, padding, Background, Border, Color, Element, Font, Length, Padding, Shadow, Theme, }; use iced_widget::container; +use iced_widget::markdown; use iced_widget::text_editor::{self, Binding, KeyPress, Motion, Status, Style}; #[derive(Debug, Clone)] pub enum Message { EditorAction(text_editor::Action), + TogglePreview, + MarkdownLink(markdown::Uri), + ZoomIn, + ZoomOut, + ZoomReset, } pub struct EditorState { pub content: text_editor::Content, pub font_size: f32, + pub preview: bool, + pub parsed: Vec, +} + +fn md_style() -> markdown::Style { + markdown::Style { + font: Font::default(), + inline_code_highlight: Highlight { + background: Color::from_rgb(0.188, 0.188, 0.259).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_font: Font::MONOSPACE, + code_block_font: Font::MONOSPACE, + link_color: Color::from_rgb(0.537, 0.706, 0.980), + } } impl EditorState { @@ -22,43 +45,99 @@ impl EditorState { Self { content: text_editor::Content::new(), font_size: 14.0, + preview: false, + parsed: Vec::new(), } } + fn reparse(&mut self) { + let text = self.content.text(); + self.parsed = markdown::parse(&text).collect(); + } + pub fn update(&mut self, message: Message) { match message { Message::EditorAction(action) => { + let is_edit = action.is_edit(); self.content.perform(action); + if is_edit { + self.reparse(); + } + } + Message::TogglePreview => { + self.preview = !self.preview; + if self.preview { + self.reparse(); + } + } + Message::MarkdownLink(_url) => {} + Message::ZoomIn => { + self.font_size = (self.font_size + 1.0).min(48.0); + } + Message::ZoomOut => { + self.font_size = (self.font_size - 1.0).max(8.0); + } + Message::ZoomReset => { + self.font_size = 14.0; } } } pub fn view(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { + let main_content: Element<'_, Message, Theme, iced_wgpu::Renderer> = if self.preview { + let settings = markdown::Settings::with_text_size(self.font_size, md_style()); + let preview = markdown::view(&self.parsed, settings) + .map(Message::MarkdownLink); + + iced_widget::container( + iced_widget::scrollable( + iced_widget::container(preview) + .padding(Padding { top: 38.0, right: 16.0, bottom: 16.0, left: 16.0 }) + ) + .height(Length::Fill) + ) + .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, + }) + .into() + } else { + iced_widget::text_editor(&self.content) + .on_action(Message::EditorAction) + .font(Font::MONOSPACE) + .size(self.font_size) + .height(Length::Fill) + .padding(Padding { top: 38.0, 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), + }) + .into() + }; + + let mode_label = if self.preview { "Preview" } else { "Edit" }; let cursor = self.content.cursor(); let line = cursor.position.line + 1; let col = cursor.position.column + 1; - let editor = iced_widget::text_editor(&self.content) - .on_action(Message::EditorAction) - .font(Font::MONOSPACE) - .size(self.font_size) - .height(Length::Fill) - .padding(Padding { top: 38.0, 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), - }); - let status_bar = iced_widget::container( - iced_widget::text(format!("Ln {}, Col {}", line, col)) - .font(Font::MONOSPACE) - .size(11.0) - .color(Color::from_rgb(0.55, 0.55, 0.55)), + iced_widget::row([ + 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)) + .into(), + ]) ) .width(Length::Fill) .padding(Padding { top: 3.0, right: 10.0, bottom: 3.0, left: 10.0 }) @@ -70,7 +149,7 @@ impl EditorState { snap: false, }); - iced_widget::column([editor.into(), status_bar.into()]) + iced_widget::column([main_content, status_bar.into()]) .height(Length::Fill) .into() } @@ -84,6 +163,18 @@ fn macos_key_binding(key_press: KeyPress) -> Option> { } match key.as_ref() { + keyboard::Key::Character("p") if modifiers.logo() => { + Some(Binding::Custom(Message::TogglePreview)) + } + keyboard::Key::Character("=" | "+") if modifiers.logo() => { + Some(Binding::Custom(Message::ZoomIn)) + } + keyboard::Key::Character("-") if modifiers.logo() => { + Some(Binding::Custom(Message::ZoomOut)) + } + keyboard::Key::Character("0") if modifiers.logo() => { + Some(Binding::Custom(Message::ZoomReset)) + } keyboard::Key::Named(key::Named::Backspace) if modifiers.alt() => { Some(Binding::Sequence(vec![ Binding::Select(Motion::WordLeft), diff --git a/viewport/src/handle.rs b/viewport/src/handle.rs index cbb6b68..d6bffaf 100644 --- a/viewport/src/handle.rs +++ b/viewport/src/handle.rs @@ -5,7 +5,7 @@ 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, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme}; +use iced_wgpu::core::{clipboard, keyboard, mouse, window, Color, Event, Font, Pixels, Point, Size, Theme}; use iced_wgpu::Engine; use raw_window_handle::{ AppKitDisplayHandle, AppKitWindowHandle, RawDisplayHandle, RawWindowHandle, @@ -157,6 +157,19 @@ pub fn render(handle: &mut ViewportHandle) { let mut clipboard = MacClipboard; let mut messages: Vec = Vec::new(); + for event in &handle.events { + if let Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Character(c), + modifiers, + .. + }) = event + { + if c.as_str() == "p" && modifiers.logo() { + messages.push(Message::TogglePreview); + } + } + } + let _ = ui.update( &handle.events, handle.cursor,