add markdown preview mode (Cmd+P), zoom in/out (Cmd+=/-/0), performKeyEquivalent forwarding

This commit is contained in:
jess 2026-04-07 18:22:11 -07:00
parent 50fcb9224a
commit 95ecdf3c95
4 changed files with 131 additions and 26 deletions

View File

@ -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"

View File

@ -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)

View File

@ -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<iced_wgpu::Renderer>,
pub font_size: f32,
pub preview: bool,
pub parsed: Vec<markdown::Item>,
}
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<Binding<Message>> {
}
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),

View File

@ -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<Message> = 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,