merge rc2 (gutter) and rc3 (syntax + auto-indent) into features branch

This commit is contained in:
jess 2026-04-07 21:09:53 -07:00
commit a61752dad8
2 changed files with 137 additions and 22 deletions

View File

@ -11,7 +11,7 @@ acord-core = { path = "../core" }
iced_wgpu = "0.14" iced_wgpu = "0.14"
iced_graphics = "0.14" iced_graphics = "0.14"
iced_runtime = "0.14" iced_runtime = "0.14"
iced_widget = { version = "0.14", features = ["wgpu", "markdown"] } iced_widget = { version = "0.14", features = ["wgpu", "markdown", "canvas"] }
wgpu = "27" wgpu = "27"
raw-window-handle = "0.6" raw-window-handle = "0.6"
pollster = "0.4" pollster = "0.4"

View File

@ -2,13 +2,15 @@ use std::sync::Arc;
use iced_wgpu::core::keyboard; use iced_wgpu::core::keyboard;
use iced_wgpu::core::keyboard::key; use iced_wgpu::core::keyboard::key;
use iced_wgpu::core::text::{Highlight, Wrapping}; use iced_wgpu::core::text::{Highlight, LineHeight, Wrapping};
use iced_wgpu::core::{ use iced_wgpu::core::{
border, padding, Background, Border, Color, Element, Font, Length, Padding, Shadow, Theme, border, padding, alignment, Background, Border, Color, Element, Font, Length,
Padding, Pixels, Point, Rectangle, Shadow, Theme,
}; };
use iced_widget::canvas;
use iced_widget::container; use iced_widget::container;
use iced_widget::markdown; use iced_widget::markdown;
use iced_widget::text_editor::{self, Binding, KeyPress, Motion, Status, Style}; use iced_widget::text_editor::{self, Action, Binding, KeyPress, Motion, Status, Style};
use iced_wgpu::core::text::highlighter::Format; use iced_wgpu::core::text::highlighter::Format;
use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings}; use crate::syntax::{self, SyntaxHighlighter, SyntaxSettings};
@ -36,6 +38,7 @@ pub struct EditorState {
pub eval_results: Vec<(usize, String)>, pub eval_results: Vec<(usize, String)>,
pub eval_errors: Vec<(usize, String)>, pub eval_errors: Vec<(usize, String)>,
pub lang: Option<String>, pub lang: Option<String>,
scroll_offset: f32,
} }
fn md_style() -> markdown::Style { fn md_style() -> markdown::Style {
@ -88,11 +91,17 @@ impl EditorState {
eval_results: Vec::new(), eval_results: Vec::new(),
eval_errors: Vec::new(), eval_errors: Vec::new(),
lang: Some("rust".into()), lang: Some("rust".into()),
scroll_offset: 0.0,
} }
} }
fn line_height(&self) -> f32 {
self.font_size * 1.3
}
pub fn set_text(&mut self, text: &str) { pub fn set_text(&mut self, text: &str) {
self.content = text_editor::Content::with_text(text); self.content = text_editor::Content::with_text(text);
self.scroll_offset = 0.0;
self.reparse(); self.reparse();
} }
@ -145,6 +154,14 @@ impl EditorState {
Message::EditorAction(action) => { Message::EditorAction(action) => {
let is_edit = action.is_edit(); let is_edit = action.is_edit();
if let Action::Scroll { lines } = &action {
let lh = self.line_height();
self.scroll_offset += *lines as f32 * lh;
self.scroll_offset = self.scroll_offset.max(0.0);
let max = (self.content.line_count() as f32 - 1.0) * lh;
self.scroll_offset = self.scroll_offset.min(max.max(0.0));
}
let auto_indent = if let text_editor::Action::Edit(text_editor::Edit::Enter) = &action { let auto_indent = if let text_editor::Action::Edit(text_editor::Edit::Enter) = &action {
let cursor = self.content.cursor(); let cursor = self.content.cursor();
let line_text = self.content.line(cursor.position.line) let line_text = self.content.line(cursor.position.line)
@ -178,7 +195,6 @@ impl EditorState {
} else { } else {
None None
}; };
self.content.perform(action); self.content.perform(action);
if let Some(indent) = auto_indent { if let Some(indent) = auto_indent {
@ -286,12 +302,13 @@ impl EditorState {
}) })
.into() .into()
} else { } else {
let top_pad = 38.0_f32;
let editor = iced_widget::text_editor(&self.content) let editor = iced_widget::text_editor(&self.content)
.on_action(Message::EditorAction) .on_action(Message::EditorAction)
.font(Font::MONOSPACE) .font(Font::MONOSPACE)
.size(self.font_size) .size(self.font_size)
.height(Length::Fill) .height(Length::Fill)
.padding(Padding { top: 38.0, right: 8.0, bottom: 8.0, left: 8.0 }) .padding(Padding { top: top_pad, right: 8.0, bottom: 8.0, left: 8.0 })
.wrapping(Wrapping::Word) .wrapping(Wrapping::Word)
.key_binding(macos_key_binding) .key_binding(macos_key_binding)
.style(|_theme, _status| Style { .style(|_theme, _status| Style {
@ -302,23 +319,43 @@ impl EditorState {
selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4), selection: Color::from_rgba(0.3, 0.5, 0.8, 0.4),
}); });
if let Some(lang) = &self.lang { let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> =
let settings = SyntaxSettings { if let Some(lang) = &self.lang {
lang: lang.clone(), let settings = SyntaxSettings {
source: self.content.text(), lang: lang.clone(),
source: self.content.text(),
};
editor
.highlight_with::<SyntaxHighlighter>(
settings,
|highlight, _theme| Format {
color: Some(syntax::highlight_color(highlight.kind)),
font: None,
},
)
.into()
} else {
editor.into()
}; };
editor
.highlight_with::<SyntaxHighlighter>( let gutter = Gutter {
settings, line_count: self.content.line_count(),
|highlight, _theme| Format { font_size: self.font_size,
color: Some(syntax::highlight_color(highlight.kind)), scroll_offset: self.scroll_offset,
font: None, cursor_line: self.content.cursor().position.line,
}, top_pad,
) };
.into() let gw = gutter.gutter_width();
} else {
editor.into() let gutter_canvas: Element<'_, Message, Theme, iced_wgpu::Renderer> =
} canvas::Canvas::new(gutter)
.width(Length::Fixed(gw))
.height(Length::Fill)
.into();
iced_widget::row![gutter_canvas, editor_el]
.height(Length::Fill)
.into()
}; };
let mode_label = if self.preview { "Preview" } else { "Edit" }; let mode_label = if self.preview { "Preview" } else { "Edit" };
@ -394,6 +431,84 @@ impl EditorState {
} }
} }
struct Gutter {
line_count: usize,
font_size: f32,
scroll_offset: f32,
cursor_line: usize,
top_pad: f32,
}
impl Gutter {
fn gutter_width(&self) -> f32 {
let digits = if self.line_count == 0 {
1
} else {
(self.line_count as f32).log10().floor() as usize + 1
};
let char_width = self.font_size * 0.6;
(digits.max(2) as f32 * char_width + 16.0).ceil()
}
}
impl canvas::Program<Message, Theme, iced_wgpu::Renderer> for Gutter {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced_wgpu::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: iced_wgpu::core::mouse::Cursor,
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let lh = self.font_size * 1.3;
frame.fill_rectangle(
Point::ORIGIN,
bounds.size(),
Color::from_rgb(0.06, 0.06, 0.08),
);
let first_visible = (self.scroll_offset / lh).floor() as usize;
let sub_pixel = self.scroll_offset - first_visible as f32 * lh;
let visible_count = (bounds.height / lh).ceil() as usize + 1;
let gw = self.gutter_width();
for i in 0..visible_count {
let line_idx = first_visible + i;
if line_idx >= self.line_count {
break;
}
let y = self.top_pad + i as f32 * lh - sub_pixel;
if y + lh < 0.0 || y > bounds.height {
continue;
}
let color = if line_idx == self.cursor_line {
Color::from_rgb(0.55, 0.55, 0.62)
} else {
Color::from_rgb(0.35, 0.35, 0.42)
};
frame.fill_text(canvas::Text {
content: format!("{}", line_idx + 1),
position: Point::new(gw - 8.0, y),
max_width: gw,
color,
size: Pixels(self.font_size),
line_height: LineHeight::Relative(1.3),
font: Font::MONOSPACE,
align_x: iced_wgpu::core::text::Alignment::Right,
align_y: alignment::Vertical::Top,
shaping: iced_wgpu::core::text::Shaping::Basic,
});
}
vec![frame.into_geometry()]
}
}
fn parse_let_binding(line: &str) -> Option<String> { fn parse_let_binding(line: &str) -> Option<String> {
let rest = line.strip_prefix("let ")?; let rest = line.strip_prefix("let ")?;
let eq_pos = rest.find('=')?; let eq_pos = rest.find('=')?;