Compare commits
No commits in common. "5b4abcf3e57da2c242ac47ec754ed49604940f60" and "01f34a4f34ca9fb7e7195551098c78d57acc4c8b" have entirely different histories.
5b4abcf3e5
...
01f34a4f34
|
|
@ -6,6 +6,11 @@ import UniformTypeIdentifiers
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let focusEditor = Notification.Name("focusEditor")
|
static let focusEditor = Notification.Name("focusEditor")
|
||||||
static let focusTitle = Notification.Name("focusTitle")
|
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 {
|
class WindowController {
|
||||||
|
|
@ -58,7 +63,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
observeDocumentTitle()
|
observeDocumentTitle()
|
||||||
|
|
||||||
observeDocumentText()
|
observeDocumentText()
|
||||||
syncThemeToViewport()
|
|
||||||
|
|
||||||
DocumentBrowserController.shared = DocumentBrowserController(appState: appState)
|
DocumentBrowserController.shared = DocumentBrowserController(appState: appState)
|
||||||
|
|
||||||
|
|
@ -277,19 +281,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func boldSelection() {
|
@objc private func boldSelection() {
|
||||||
viewport?.sendCommand(1)
|
NotificationCenter.default.post(name: .boldSelection, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func italicizeSelection() {
|
@objc private func italicizeSelection() {
|
||||||
viewport?.sendCommand(2)
|
NotificationCenter.default.post(name: .italicizeSelection, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func insertTable() {
|
@objc private func insertTable() {
|
||||||
viewport?.sendCommand(3)
|
NotificationCenter.default.post(name: .insertTable, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func smartEval() {
|
@objc private func smartEval() {
|
||||||
viewport?.sendCommand(4)
|
NotificationCenter.default.post(name: .smartEval, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func openNote() {
|
@objc private func openNote() {
|
||||||
|
|
@ -408,7 +412,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@objc private func formatDocument() {
|
@objc private func formatDocument() {
|
||||||
viewport?.sendCommand(10)
|
NotificationCenter.default.post(name: .formatDocument, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func openSettings() {
|
@objc private func openSettings() {
|
||||||
|
|
@ -417,24 +421,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@objc private func settingsDidChange() {
|
@objc private func settingsDidChange() {
|
||||||
window.backgroundColor = Theme.current.base
|
window.backgroundColor = Theme.current.base
|
||||||
syncThemeToViewport()
|
|
||||||
window.contentView?.needsDisplay = true
|
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() {
|
@objc private func toggleBrowser() {
|
||||||
DocumentBrowserController.shared?.toggle()
|
DocumentBrowserController.shared?.toggle()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ class IcedViewportView: NSView {
|
||||||
|
|
||||||
if cmd && !shift {
|
if cmd && !shift {
|
||||||
switch chars {
|
switch chars {
|
||||||
case "a", "b", "c", "e", "f", "g", "i", "v", "x", "z", "p", "t",
|
case "a", "b", "c", "i", "v", "x", "z", "p", "t",
|
||||||
"=", "+", "-", "0":
|
"=", "+", "-", "0":
|
||||||
keyDown(with: event)
|
keyDown(with: event)
|
||||||
return true
|
return true
|
||||||
|
|
@ -168,7 +168,7 @@ class IcedViewportView: NSView {
|
||||||
}
|
}
|
||||||
if cmd && shift {
|
if cmd && shift {
|
||||||
switch chars {
|
switch chars {
|
||||||
case "g", "z":
|
case "z":
|
||||||
keyDown(with: event)
|
keyDown(with: event)
|
||||||
return true
|
return true
|
||||||
default: break
|
default: break
|
||||||
|
|
@ -214,16 +214,4 @@ class IcedViewportView: NSView {
|
||||||
viewport_free_string(cstr)
|
viewport_free_string(cstr)
|
||||||
return result
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,4 @@ char *viewport_get_text(struct ViewportHandle *handle);
|
||||||
|
|
||||||
void viewport_free_string(char *s);
|
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 */
|
#endif /* ACORD_VIEWPORT_H */
|
||||||
|
|
|
||||||
|
|
@ -1,476 +0,0 @@
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use iced_widget::text_editor;
|
|
||||||
|
|
||||||
static NEXT_BLOCK_ID: AtomicU64 = AtomicU64::new(1);
|
|
||||||
|
|
||||||
pub fn next_id() -> u64 {
|
|
||||||
NEXT_BLOCK_ID.fetch_add(1, Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum BlockKind {
|
|
||||||
Text,
|
|
||||||
Table,
|
|
||||||
HorizontalRule,
|
|
||||||
Heading,
|
|
||||||
Tree,
|
|
||||||
EvalResult,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Block {
|
|
||||||
pub id: u64,
|
|
||||||
pub kind: BlockKind,
|
|
||||||
pub content: text_editor::Content<iced_wgpu::Renderer>,
|
|
||||||
pub start_line: usize,
|
|
||||||
pub line_count: usize,
|
|
||||||
pub heading_level: u8,
|
|
||||||
pub heading_text: String,
|
|
||||||
pub table_rows: Vec<Vec<String>>,
|
|
||||||
pub tree_data: Option<serde_json::Value>,
|
|
||||||
pub eval_text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Block {
|
|
||||||
pub fn text(text: &str, start_line: usize) -> Self {
|
|
||||||
let lc = text.lines().count().max(1);
|
|
||||||
Self {
|
|
||||||
id: next_id(),
|
|
||||||
kind: BlockKind::Text,
|
|
||||||
content: text_editor::Content::with_text(text),
|
|
||||||
start_line,
|
|
||||||
line_count: lc,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
tree_data: None,
|
|
||||||
eval_text: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn heading(level: u8, text: String, line: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
id: next_id(),
|
|
||||||
kind: BlockKind::Heading,
|
|
||||||
content: text_editor::Content::with_text(""),
|
|
||||||
start_line: line,
|
|
||||||
line_count: 1,
|
|
||||||
heading_level: level,
|
|
||||||
heading_text: text,
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
tree_data: None,
|
|
||||||
eval_text: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hr(line: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
id: next_id(),
|
|
||||||
kind: BlockKind::HorizontalRule,
|
|
||||||
content: text_editor::Content::with_text(""),
|
|
||||||
start_line: line,
|
|
||||||
line_count: 1,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
tree_data: None,
|
|
||||||
eval_text: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn table(rows: Vec<Vec<String>>, start_line: usize, line_count: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
id: next_id(),
|
|
||||||
kind: BlockKind::Table,
|
|
||||||
content: text_editor::Content::with_text(""),
|
|
||||||
start_line,
|
|
||||||
line_count,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: rows,
|
|
||||||
tree_data: None,
|
|
||||||
eval_text: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_result(text: String, line: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
id: next_id(),
|
|
||||||
kind: BlockKind::EvalResult,
|
|
||||||
content: text_editor::Content::with_text(""),
|
|
||||||
start_line: line,
|
|
||||||
line_count: 1,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
tree_data: None,
|
|
||||||
eval_text: text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tree(data: serde_json::Value, line: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
id: next_id(),
|
|
||||||
kind: BlockKind::Tree,
|
|
||||||
content: text_editor::Content::with_text(""),
|
|
||||||
start_line: line,
|
|
||||||
line_count: 1,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
tree_data: Some(data),
|
|
||||||
eval_text: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_hr_line(line: &str) -> bool {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
trimmed.len() >= 3 && trimmed.chars().all(|c| c == '-')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn heading_prefix(line: &str) -> Option<(u8, &str)> {
|
|
||||||
let trimmed = line.trim_start();
|
|
||||||
let bytes = trimmed.as_bytes();
|
|
||||||
if bytes.is_empty() || bytes[0] != b'#' {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut level = 0u8;
|
|
||||||
while (level as usize) < bytes.len() && bytes[level as usize] == b'#' {
|
|
||||||
level += 1;
|
|
||||||
}
|
|
||||||
if level > 3 || (level as usize) >= bytes.len() || bytes[level as usize] != b' ' {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some((level, &trimmed[level as usize + 1..]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_table_start(lines: &[&str], idx: usize) -> bool {
|
|
||||||
if idx + 1 >= lines.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let line = lines[idx].trim();
|
|
||||||
let next = lines[idx + 1].trim();
|
|
||||||
if !line.starts_with('|') || !next.starts_with('|') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let inner = next.strip_prefix('|').unwrap_or(next);
|
|
||||||
let inner = inner.strip_suffix('|').unwrap_or(inner);
|
|
||||||
inner.split('|').all(|seg| {
|
|
||||||
let s = seg.trim();
|
|
||||||
!s.is_empty() && s.chars().all(|c| c == '-' || c == ':')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_table(lines: &[&str], start: usize) -> (Vec<Vec<String>>, usize) {
|
|
||||||
let parse_row = |line: &str| -> Vec<String> {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
let inner = trimmed.strip_prefix('|').unwrap_or(trimmed);
|
|
||||||
let inner = inner.strip_suffix('|').unwrap_or(inner);
|
|
||||||
inner.split('|').map(|c| c.trim().to_string()).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut rows = vec![parse_row(lines[start])];
|
|
||||||
let mut end = start + 2; // skip header + separator
|
|
||||||
while end < lines.len() {
|
|
||||||
let trimmed = lines[end].trim();
|
|
||||||
if trimmed.is_empty() || !trimmed.contains('|') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rows.push(parse_row(lines[end]));
|
|
||||||
end += 1;
|
|
||||||
}
|
|
||||||
(rows, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Classify each line into a block kind, returning (kind, line_index) boundaries.
|
|
||||||
struct BlockSpan {
|
|
||||||
kind: BlockKind,
|
|
||||||
start: usize,
|
|
||||||
end: usize, // exclusive
|
|
||||||
heading_level: u8,
|
|
||||||
heading_text: String,
|
|
||||||
table_rows: Vec<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn classify_spans(lines: &[&str]) -> Vec<BlockSpan> {
|
|
||||||
let mut spans = Vec::new();
|
|
||||||
let mut i = 0;
|
|
||||||
let mut text_start: Option<usize> = None;
|
|
||||||
|
|
||||||
let flush_text = |text_start: &mut Option<usize>, i: usize, spans: &mut Vec<BlockSpan>| {
|
|
||||||
if let Some(s) = text_start.take() {
|
|
||||||
if s < i {
|
|
||||||
spans.push(BlockSpan {
|
|
||||||
kind: BlockKind::Text,
|
|
||||||
start: s,
|
|
||||||
end: i,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while i < lines.len() {
|
|
||||||
if is_hr_line(lines[i]) {
|
|
||||||
flush_text(&mut text_start, i, &mut spans);
|
|
||||||
spans.push(BlockSpan {
|
|
||||||
kind: BlockKind::HorizontalRule,
|
|
||||||
start: i,
|
|
||||||
end: i + 1,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
});
|
|
||||||
i += 1;
|
|
||||||
} else if let Some((level, text)) = heading_prefix(lines[i]) {
|
|
||||||
flush_text(&mut text_start, i, &mut spans);
|
|
||||||
spans.push(BlockSpan {
|
|
||||||
kind: BlockKind::Heading,
|
|
||||||
start: i,
|
|
||||||
end: i + 1,
|
|
||||||
heading_level: level,
|
|
||||||
heading_text: text.to_string(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
});
|
|
||||||
i += 1;
|
|
||||||
} else if is_table_start(lines, i) {
|
|
||||||
flush_text(&mut text_start, i, &mut spans);
|
|
||||||
let (rows, end) = consume_table(lines, i);
|
|
||||||
spans.push(BlockSpan {
|
|
||||||
kind: BlockKind::Table,
|
|
||||||
start: i,
|
|
||||||
end,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: rows,
|
|
||||||
});
|
|
||||||
i = end;
|
|
||||||
} else {
|
|
||||||
if text_start.is_none() {
|
|
||||||
text_start = Some(i);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flush_text(&mut text_start, lines.len(), &mut spans);
|
|
||||||
|
|
||||||
if spans.is_empty() {
|
|
||||||
spans.push(BlockSpan {
|
|
||||||
kind: BlockKind::Text,
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
heading_level: 0,
|
|
||||||
heading_text: String::new(),
|
|
||||||
table_rows: Vec::new(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
spans
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse document text into blocks, detecting HR, headings, and tables.
|
|
||||||
pub fn parse_blocks(text: &str) -> Vec<Block> {
|
|
||||||
if text.is_empty() {
|
|
||||||
return vec![Block::text("", 0)];
|
|
||||||
}
|
|
||||||
|
|
||||||
let lines: Vec<&str> = text.lines().collect();
|
|
||||||
let spans = classify_spans(&lines);
|
|
||||||
let mut blocks = Vec::with_capacity(spans.len());
|
|
||||||
|
|
||||||
for span in &spans {
|
|
||||||
match span.kind {
|
|
||||||
BlockKind::Text => {
|
|
||||||
let block_text = lines[span.start..span.end].join("\n");
|
|
||||||
blocks.push(Block::text(&block_text, span.start));
|
|
||||||
}
|
|
||||||
BlockKind::HorizontalRule => {
|
|
||||||
blocks.push(Block::hr(span.start));
|
|
||||||
}
|
|
||||||
BlockKind::Heading => {
|
|
||||||
blocks.push(Block::heading(span.heading_level, span.heading_text.clone(), span.start));
|
|
||||||
}
|
|
||||||
BlockKind::Table => {
|
|
||||||
blocks.push(Block::table(span.table_rows.clone(), span.start, span.end - span.start));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if blocks.is_empty() {
|
|
||||||
blocks.push(Block::text("", 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Incremental reparse: compare old block kinds to new, reuse unchanged blocks.
|
|
||||||
/// Returns the new block list, preserving IDs and content of blocks that didn't change type.
|
|
||||||
pub fn reparse_incremental(old_blocks: &mut Vec<Block>, text: &str) {
|
|
||||||
let lines: Vec<&str> = if text.is_empty() {
|
|
||||||
Vec::new()
|
|
||||||
} else {
|
|
||||||
text.lines().collect()
|
|
||||||
};
|
|
||||||
let spans = classify_spans(&lines);
|
|
||||||
|
|
||||||
let old_kinds: Vec<(BlockKind, usize, usize)> = old_blocks
|
|
||||||
.iter()
|
|
||||||
.map(|b| (b.kind, b.start_line, b.line_count))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let new_kinds: Vec<(BlockKind, usize, usize)> = spans
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.kind, s.start, s.end - s.start))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if old_kinds == new_kinds {
|
|
||||||
// Same structure: update text content in-place for text blocks
|
|
||||||
for (block, span) in old_blocks.iter_mut().zip(spans.iter()) {
|
|
||||||
block.start_line = span.start;
|
|
||||||
block.line_count = span.end - span.start;
|
|
||||||
if block.kind == BlockKind::Text {
|
|
||||||
let block_text = lines[span.start..span.end].join("\n");
|
|
||||||
let current = block.content.text();
|
|
||||||
if current != block_text {
|
|
||||||
block.content = text_editor::Content::with_text(&block_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Structure changed: rebuild, but try to reuse blocks at matching positions
|
|
||||||
let mut new_blocks = Vec::with_capacity(spans.len());
|
|
||||||
for (i, span) in spans.iter().enumerate() {
|
|
||||||
let reuse = if i < old_blocks.len()
|
|
||||||
&& old_blocks[i].kind == span.kind
|
|
||||||
&& old_blocks[i].start_line == span.start
|
|
||||||
{
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if reuse {
|
|
||||||
let mut b = std::mem::replace(&mut old_blocks[i], Block::text("", 0));
|
|
||||||
b.start_line = span.start;
|
|
||||||
b.line_count = span.end - span.start;
|
|
||||||
if b.kind == BlockKind::Text {
|
|
||||||
let block_text = lines[span.start..span.end].join("\n");
|
|
||||||
let current = b.content.text();
|
|
||||||
if current != block_text {
|
|
||||||
b.content = text_editor::Content::with_text(&block_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
new_blocks.push(b);
|
|
||||||
} else {
|
|
||||||
match span.kind {
|
|
||||||
BlockKind::Text => {
|
|
||||||
let block_text = lines[span.start..span.end].join("\n");
|
|
||||||
new_blocks.push(Block::text(&block_text, span.start));
|
|
||||||
}
|
|
||||||
BlockKind::HorizontalRule => {
|
|
||||||
new_blocks.push(Block::hr(span.start));
|
|
||||||
}
|
|
||||||
BlockKind::Heading => {
|
|
||||||
new_blocks.push(Block::heading(span.heading_level, span.heading_text.clone(), span.start));
|
|
||||||
}
|
|
||||||
BlockKind::Table => {
|
|
||||||
new_blocks.push(Block::table(span.table_rows.clone(), span.start, span.end - span.start));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_blocks.is_empty() {
|
|
||||||
new_blocks.push(Block::text("", 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
*old_blocks = new_blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize blocks back to document text.
|
|
||||||
pub fn serialize_blocks(blocks: &[Block]) -> String {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
for block in blocks {
|
|
||||||
match block.kind {
|
|
||||||
BlockKind::Text => {
|
|
||||||
parts.push(block.content.text());
|
|
||||||
}
|
|
||||||
BlockKind::Table => {
|
|
||||||
if let Some(header) = block.table_rows.first() {
|
|
||||||
let cells: Vec<&str> = header.iter().map(|s| s.as_str()).collect();
|
|
||||||
parts.push(format!("| {} |", cells.join(" | ")));
|
|
||||||
let sep = header.iter()
|
|
||||||
.map(|c| "-".repeat(c.len().max(3)))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" | ");
|
|
||||||
parts.push(format!("| {} |", sep));
|
|
||||||
for row in block.table_rows.iter().skip(1) {
|
|
||||||
let cells: Vec<&str> = row.iter().map(|s| s.as_str()).collect();
|
|
||||||
parts.push(format!("| {} |", cells.join(" | ")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BlockKind::HorizontalRule => {
|
|
||||||
parts.push("---".to_string());
|
|
||||||
}
|
|
||||||
BlockKind::Heading => {
|
|
||||||
let prefix = "#".repeat(block.heading_level as usize);
|
|
||||||
parts.push(format!("{prefix} {}", block.heading_text));
|
|
||||||
}
|
|
||||||
BlockKind::Tree => {}
|
|
||||||
BlockKind::EvalResult => {
|
|
||||||
parts.push(block.eval_text.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract plain text from all TextBlocks.
|
|
||||||
pub fn collect_text(blocks: &[Block]) -> String {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
for block in blocks {
|
|
||||||
if block.kind == BlockKind::Text {
|
|
||||||
parts.push(block.content.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parts.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Merge two adjacent text blocks. Returns the merged text.
|
|
||||||
pub fn merge_text_blocks(first: &Block, second: &Block) -> String {
|
|
||||||
let a = first.content.text();
|
|
||||||
let b = second.content.text();
|
|
||||||
if a.is_empty() {
|
|
||||||
b
|
|
||||||
} else if b.is_empty() {
|
|
||||||
a
|
|
||||||
} else {
|
|
||||||
format!("{}\n{}", a, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update start_line counts after blocks change.
|
|
||||||
pub fn recount_lines(blocks: &mut [Block]) {
|
|
||||||
let mut line = 0;
|
|
||||||
for block in blocks.iter_mut() {
|
|
||||||
block.start_line = line;
|
|
||||||
line += block.line_count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the block index containing a given global line number.
|
|
||||||
pub fn block_at_line(blocks: &[Block], global_line: usize) -> Option<usize> {
|
|
||||||
for (i, block) in blocks.iter().enumerate() {
|
|
||||||
if global_line >= block.start_line && global_line < block.start_line + block.line_count {
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,70 +0,0 @@
|
||||||
use iced_wgpu::core::text::LineHeight;
|
|
||||||
use iced_wgpu::core::{
|
|
||||||
alignment, Element, Font, Length, Pixels, Point, Rectangle, Theme,
|
|
||||||
};
|
|
||||||
use iced_widget::canvas;
|
|
||||||
|
|
||||||
use crate::palette;
|
|
||||||
|
|
||||||
const FONT_SIZE: f32 = 13.0;
|
|
||||||
const LINE_HEIGHT: f32 = 20.0;
|
|
||||||
const LEFT_MARGIN: f32 = 8.0;
|
|
||||||
const PADDING: f32 = 4.0;
|
|
||||||
|
|
||||||
pub struct EvalResultProgram {
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EvalResultProgram {
|
|
||||||
pub fn new(result_text: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
text: format!("\u{2192} {}", result_text),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn height() -> f32 {
|
|
||||||
LINE_HEIGHT + PADDING * 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for EvalResultProgram {
|
|
||||||
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 p = palette::current();
|
|
||||||
|
|
||||||
frame.fill_text(canvas::Text {
|
|
||||||
content: self.text.clone(),
|
|
||||||
position: Point::new(LEFT_MARGIN, PADDING),
|
|
||||||
max_width: bounds.width - LEFT_MARGIN * 2.0,
|
|
||||||
color: p.green,
|
|
||||||
size: Pixels(FONT_SIZE),
|
|
||||||
line_height: LineHeight::Relative(1.3),
|
|
||||||
font: Font::MONOSPACE,
|
|
||||||
align_x: alignment::Horizontal::Left.into(),
|
|
||||||
align_y: alignment::Vertical::Top,
|
|
||||||
shaping: iced_wgpu::core::text::Shaping::Basic,
|
|
||||||
});
|
|
||||||
|
|
||||||
vec![frame.into_geometry()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view<'a, Message: Clone + 'a>(
|
|
||||||
result_text: &str,
|
|
||||||
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
|
||||||
let program = EvalResultProgram::new(result_text);
|
|
||||||
let h = EvalResultProgram::height();
|
|
||||||
canvas::Canvas::new(program)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(h))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,6 @@ use raw_window_handle::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::editor::{EditorState, Message};
|
use crate::editor::{EditorState, Message};
|
||||||
use crate::palette;
|
|
||||||
use crate::ViewportHandle;
|
use crate::ViewportHandle;
|
||||||
|
|
||||||
struct MacClipboard;
|
struct MacClipboard;
|
||||||
|
|
@ -159,35 +158,22 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
let mut messages: Vec<Message> = Vec::new();
|
let mut messages: Vec<Message> = Vec::new();
|
||||||
|
|
||||||
for event in &handle.events {
|
for event in &handle.events {
|
||||||
match event {
|
if let Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
|
||||||
key: keyboard::Key::Character(c),
|
key: keyboard::Key::Character(c),
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
..
|
||||||
}) if modifiers.logo() => {
|
}) = event
|
||||||
|
{
|
||||||
|
if modifiers.logo() {
|
||||||
match c.as_str() {
|
match c.as_str() {
|
||||||
"p" => messages.push(Message::TogglePreview),
|
"p" => messages.push(Message::TogglePreview),
|
||||||
"t" => messages.push(Message::InsertTable),
|
"t" => messages.push(Message::InsertTable),
|
||||||
"b" => messages.push(Message::ToggleBold),
|
"b" => messages.push(Message::ToggleBold),
|
||||||
"i" => messages.push(Message::ToggleItalic),
|
"i" => messages.push(Message::ToggleItalic),
|
||||||
"e" => messages.push(Message::SmartEval),
|
"e" => messages.push(Message::SmartEval),
|
||||||
"z" if modifiers.shift() => messages.push(Message::Redo),
|
|
||||||
"z" => messages.push(Message::Undo),
|
|
||||||
"f" => messages.push(Message::ToggleFind),
|
|
||||||
"g" if modifiers.shift() => messages.push(Message::FindPrev),
|
|
||||||
"g" => messages.push(Message::FindNext),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
|
||||||
key: keyboard::Key::Named(keyboard::key::Named::Escape),
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
if handle.state.find.visible {
|
|
||||||
messages.push(Message::HideFind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,9 +192,6 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
handle.state.update(msg);
|
handle.state.update(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
handle.state.tick();
|
|
||||||
let pending_focus = handle.state.take_pending_focus();
|
|
||||||
|
|
||||||
let theme = Theme::Dark;
|
let theme = Theme::Dark;
|
||||||
let style = Style {
|
let style = Style {
|
||||||
text_color: Color::WHITE,
|
text_color: Color::WHITE,
|
||||||
|
|
@ -221,18 +204,13 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
&mut handle.renderer,
|
&mut handle.renderer,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(focus_id) = pending_focus {
|
|
||||||
use iced_wgpu::core::widget::operation::focusable;
|
|
||||||
let mut op = focusable::focus(focus_id);
|
|
||||||
ui.operate(&handle.renderer, &mut op);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
|
ui.draw(&mut handle.renderer, &theme, &style, handle.cursor);
|
||||||
handle.cache = ui.into_cache();
|
handle.cache = ui.into_cache();
|
||||||
|
|
||||||
|
let bg = Color::from_rgb(0.08, 0.08, 0.10);
|
||||||
handle
|
handle
|
||||||
.renderer
|
.renderer
|
||||||
.present(Some(palette::current().base), handle.format, &view, &handle.viewport);
|
.present(Some(bg), handle.format, &view, &handle.viewport);
|
||||||
|
|
||||||
frame.present();
|
frame.present();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
use iced_wgpu::core::alignment;
|
|
||||||
use iced_wgpu::core::text::LineHeight;
|
|
||||||
use iced_wgpu::core::{
|
|
||||||
mouse, Element, Font, Length, Pixels, Point, Rectangle, Theme,
|
|
||||||
};
|
|
||||||
use iced_wgpu::core::font::Weight;
|
|
||||||
use iced_widget::canvas;
|
|
||||||
|
|
||||||
use crate::palette;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum HeadingLevel {
|
|
||||||
H1,
|
|
||||||
H2,
|
|
||||||
H3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HeadingLevel {
|
|
||||||
pub fn scale(&self) -> f32 {
|
|
||||||
match self {
|
|
||||||
HeadingLevel::H1 => 2.0,
|
|
||||||
HeadingLevel::H2 => 1.5,
|
|
||||||
HeadingLevel::H3 => 1.17,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn weight(&self) -> Weight {
|
|
||||||
match self {
|
|
||||||
HeadingLevel::H1 => Weight::Black,
|
|
||||||
HeadingLevel::H2 => Weight::Bold,
|
|
||||||
HeadingLevel::H3 => Weight::Semibold,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_prefix(line: &str) -> Option<(HeadingLevel, String)> {
|
|
||||||
let trimmed = line.trim_start();
|
|
||||||
let bytes = trimmed.as_bytes();
|
|
||||||
if bytes.is_empty() || bytes[0] != b'#' {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut level = 0;
|
|
||||||
while level < bytes.len() && bytes[level] == b'#' {
|
|
||||||
level += 1;
|
|
||||||
}
|
|
||||||
if level > 3 || level >= bytes.len() || bytes[level] != b' ' {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let text = trimmed[level + 1..].to_string();
|
|
||||||
let lvl = match level {
|
|
||||||
1 => HeadingLevel::H1,
|
|
||||||
2 => HeadingLevel::H2,
|
|
||||||
_ => HeadingLevel::H3,
|
|
||||||
};
|
|
||||||
Some((lvl, text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HeadingProgram {
|
|
||||||
level: HeadingLevel,
|
|
||||||
text: String,
|
|
||||||
font_size: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for HeadingProgram {
|
|
||||||
type State = ();
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
_state: &(),
|
|
||||||
renderer: &iced_wgpu::Renderer,
|
|
||||||
_theme: &Theme,
|
|
||||||
bounds: Rectangle,
|
|
||||||
_cursor: mouse::Cursor,
|
|
||||||
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
|
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
|
||||||
let p = palette::current();
|
|
||||||
|
|
||||||
let color = match self.level {
|
|
||||||
HeadingLevel::H1 => p.rosewater,
|
|
||||||
HeadingLevel::H2 => p.peach,
|
|
||||||
HeadingLevel::H3 => p.yellow,
|
|
||||||
};
|
|
||||||
|
|
||||||
frame.fill_text(canvas::Text {
|
|
||||||
content: self.text.clone(),
|
|
||||||
position: Point::new(8.0, 4.0),
|
|
||||||
max_width: bounds.width - 16.0,
|
|
||||||
color,
|
|
||||||
size: Pixels(self.font_size),
|
|
||||||
line_height: LineHeight::Relative(1.4),
|
|
||||||
font: Font { weight: self.level.weight(), ..Font::DEFAULT },
|
|
||||||
align_x: iced_wgpu::core::text::Alignment::Left,
|
|
||||||
align_y: alignment::Vertical::Top,
|
|
||||||
shaping: iced_wgpu::core::text::Shaping::Basic,
|
|
||||||
});
|
|
||||||
|
|
||||||
vec![frame.into_geometry()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view<'a, Message: Clone + 'a>(
|
|
||||||
level: HeadingLevel,
|
|
||||||
text: &str,
|
|
||||||
base_font_size: f32,
|
|
||||||
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
|
||||||
let font_size = base_font_size * level.scale();
|
|
||||||
let height = font_size * 1.4 + 8.0;
|
|
||||||
canvas::Canvas::new(HeadingProgram {
|
|
||||||
level,
|
|
||||||
text: text.to_string(),
|
|
||||||
font_size,
|
|
||||||
})
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(height))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
use iced_wgpu::core::{mouse, Element, Length, Point, Rectangle, Theme};
|
|
||||||
use iced_widget::canvas;
|
|
||||||
|
|
||||||
use crate::palette;
|
|
||||||
|
|
||||||
struct HRProgram;
|
|
||||||
|
|
||||||
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for HRProgram {
|
|
||||||
type State = ();
|
|
||||||
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
_state: &(),
|
|
||||||
renderer: &iced_wgpu::Renderer,
|
|
||||||
_theme: &Theme,
|
|
||||||
bounds: Rectangle,
|
|
||||||
_cursor: mouse::Cursor,
|
|
||||||
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
|
|
||||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
|
||||||
let p = palette::current();
|
|
||||||
let y = bounds.height / 2.0;
|
|
||||||
let margin = 8.0;
|
|
||||||
let path = canvas::Path::line(
|
|
||||||
Point::new(margin, y),
|
|
||||||
Point::new(bounds.width - margin, y),
|
|
||||||
);
|
|
||||||
frame.stroke(
|
|
||||||
&path,
|
|
||||||
canvas::Stroke::default()
|
|
||||||
.with_width(1.0)
|
|
||||||
.with_color(p.overlay0),
|
|
||||||
);
|
|
||||||
vec![frame.into_geometry()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view<'a, Message: Clone + 'a>() -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
|
||||||
canvas::Canvas::new(HRProgram)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(20.0))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,9 @@
|
||||||
use std::ffi::{c_char, c_void, CStr, CString};
|
use std::ffi::{c_char, c_void, CStr, CString};
|
||||||
|
|
||||||
pub mod blocks;
|
|
||||||
mod bridge;
|
mod bridge;
|
||||||
mod editor;
|
mod editor;
|
||||||
pub mod eval_block;
|
|
||||||
mod handle;
|
mod handle;
|
||||||
pub mod heading_block;
|
|
||||||
pub mod hr_block;
|
|
||||||
pub mod palette;
|
|
||||||
mod syntax;
|
mod syntax;
|
||||||
pub mod table_block;
|
|
||||||
pub mod tree_block;
|
|
||||||
|
|
||||||
pub use acord_core::*;
|
pub use acord_core::*;
|
||||||
|
|
||||||
|
|
@ -179,34 +172,3 @@ pub extern "C" fn viewport_free_string(s: *mut c_char) {
|
||||||
if s.is_null() { return; }
|
if s.is_null() { return; }
|
||||||
unsafe { drop(CString::from_raw(s)); }
|
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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +1,13 @@
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use iced_wgpu::core::text::highlighter;
|
use iced_wgpu::core::text::highlighter;
|
||||||
use iced_wgpu::core::{Color, Font};
|
use iced_wgpu::core::Color;
|
||||||
use iced_wgpu::core::font::{Weight, Style as FontStyle};
|
|
||||||
use acord_core::highlight::{highlight_source, HighlightSpan};
|
use acord_core::highlight::{highlight_source, HighlightSpan};
|
||||||
use acord_core::doc::{classify_document, LineKind};
|
|
||||||
use crate::editor::{RESULT_PREFIX, ERROR_PREFIX};
|
use crate::editor::{RESULT_PREFIX, ERROR_PREFIX};
|
||||||
use crate::palette;
|
|
||||||
|
|
||||||
pub const EVAL_RESULT_KIND: u8 = 24;
|
pub const EVAL_RESULT_KIND: u8 = 24;
|
||||||
pub const EVAL_ERROR_KIND: u8 = 25;
|
pub const EVAL_ERROR_KIND: u8 = 25;
|
||||||
|
|
||||||
const MD_HEADING_MARKER: u8 = 26;
|
|
||||||
const MD_H1: u8 = 27;
|
|
||||||
const MD_H2: u8 = 28;
|
|
||||||
const MD_H3: u8 = 29;
|
|
||||||
const MD_BOLD: u8 = 30;
|
|
||||||
const MD_ITALIC: u8 = 31;
|
|
||||||
const MD_INLINE_CODE: u8 = 32;
|
|
||||||
const MD_FORMAT_MARKER: u8 = 33;
|
|
||||||
const MD_LINK_TEXT: u8 = 34;
|
|
||||||
const MD_LINK_URL: u8 = 35;
|
|
||||||
const MD_BLOCKQUOTE_MARKER: u8 = 36;
|
|
||||||
const MD_BLOCKQUOTE: u8 = 37;
|
|
||||||
const MD_LIST_MARKER: u8 = 38;
|
|
||||||
const MD_FENCE_MARKER: u8 = 39;
|
|
||||||
const MD_CODE_BLOCK: u8 = 40;
|
|
||||||
const MD_HR: u8 = 41;
|
|
||||||
const MD_TASK_OPEN: u8 = 42;
|
|
||||||
const MD_TASK_DONE: u8 = 43;
|
|
||||||
const MD_BOLD_ITALIC: u8 = 44;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct SyntaxSettings {
|
pub struct SyntaxSettings {
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
|
@ -42,23 +19,11 @@ pub struct SyntaxHighlight {
|
||||||
pub kind: u8,
|
pub kind: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum LineDecor {
|
|
||||||
None,
|
|
||||||
CodeBlock,
|
|
||||||
Blockquote,
|
|
||||||
HorizontalRule,
|
|
||||||
FenceMarker,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SyntaxHighlighter {
|
pub struct SyntaxHighlighter {
|
||||||
lang: String,
|
lang: String,
|
||||||
spans: Vec<HighlightSpan>,
|
spans: Vec<HighlightSpan>,
|
||||||
line_offsets: Vec<usize>,
|
line_offsets: Vec<usize>,
|
||||||
line_kinds: Vec<LineKind>,
|
|
||||||
in_fenced_code: bool,
|
|
||||||
current_line: usize,
|
current_line: usize,
|
||||||
line_decors: Vec<LineDecor>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyntaxHighlighter {
|
impl SyntaxHighlighter {
|
||||||
|
|
@ -70,323 +35,8 @@ impl SyntaxHighlighter {
|
||||||
self.line_offsets.push(offset);
|
self.line_offsets.push(offset);
|
||||||
offset += line.len() + 1;
|
offset += line.len() + 1;
|
||||||
}
|
}
|
||||||
let classified = classify_document(source);
|
|
||||||
self.line_kinds = classified.into_iter().map(|cl| cl.kind).collect();
|
|
||||||
|
|
||||||
self.line_decors.clear();
|
|
||||||
let mut in_fence = false;
|
|
||||||
for (i, raw_line) in source.split('\n').enumerate() {
|
|
||||||
let is_md = i < self.line_kinds.len() && self.line_kinds[i] == LineKind::Markdown;
|
|
||||||
if is_md {
|
|
||||||
let trimmed = raw_line.trim_start();
|
|
||||||
if trimmed.starts_with("```") {
|
|
||||||
in_fence = !in_fence;
|
|
||||||
self.line_decors.push(LineDecor::FenceMarker);
|
|
||||||
} else if in_fence {
|
|
||||||
self.line_decors.push(LineDecor::CodeBlock);
|
|
||||||
} else if is_horizontal_rule(trimmed) {
|
|
||||||
self.line_decors.push(LineDecor::HorizontalRule);
|
|
||||||
} else if trimmed.starts_with("> ") || trimmed == ">" {
|
|
||||||
self.line_decors.push(LineDecor::Blockquote);
|
|
||||||
} else {
|
|
||||||
self.line_decors.push(LineDecor::None);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if in_fence { in_fence = false; }
|
|
||||||
self.line_decors.push(LineDecor::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.in_fenced_code = false;
|
|
||||||
self.current_line = 0;
|
self.current_line = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_markdown(&self, line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> {
|
|
||||||
let trimmed = line.trim_start();
|
|
||||||
let leading = line.len() - trimmed.len();
|
|
||||||
|
|
||||||
if is_horizontal_rule(trimmed) {
|
|
||||||
return vec![(0..line.len(), SyntaxHighlight { kind: MD_HR })];
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(level) = heading_level(trimmed) {
|
|
||||||
let marker_end = leading + level + 1;
|
|
||||||
let kind = match level {
|
|
||||||
1 => MD_H1,
|
|
||||||
2 => MD_H2,
|
|
||||||
_ => MD_H3,
|
|
||||||
};
|
|
||||||
let mut spans = vec![
|
|
||||||
(0..marker_end, SyntaxHighlight { kind: MD_HEADING_MARKER }),
|
|
||||||
];
|
|
||||||
if marker_end < line.len() {
|
|
||||||
spans.push((marker_end..line.len(), SyntaxHighlight { kind }));
|
|
||||||
}
|
|
||||||
return spans;
|
|
||||||
}
|
|
||||||
|
|
||||||
if trimmed.starts_with("> ") || trimmed == ">" {
|
|
||||||
let marker_end = leading + if trimmed.len() > 1 { 2 } else { 1 };
|
|
||||||
let mut spans = vec![
|
|
||||||
(0..marker_end, SyntaxHighlight { kind: MD_BLOCKQUOTE_MARKER }),
|
|
||||||
];
|
|
||||||
if marker_end < line.len() {
|
|
||||||
let content = &line[marker_end..];
|
|
||||||
let inner = parse_inline(content, marker_end);
|
|
||||||
if inner.is_empty() {
|
|
||||||
spans.push((marker_end..line.len(), SyntaxHighlight { kind: MD_BLOCKQUOTE }));
|
|
||||||
} else {
|
|
||||||
spans.extend(inner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return spans;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(list_info) = list_marker_info(trimmed) {
|
|
||||||
let (marker_len, marker_kind) = match list_info {
|
|
||||||
ListKind::TaskOpen(n) => (n, MD_TASK_OPEN),
|
|
||||||
ListKind::TaskDone(n) => (n, MD_TASK_DONE),
|
|
||||||
ListKind::Plain(n) => (n, MD_LIST_MARKER),
|
|
||||||
};
|
|
||||||
let marker_end = leading + marker_len;
|
|
||||||
let mut spans = vec![
|
|
||||||
(0..marker_end, SyntaxHighlight { kind: marker_kind }),
|
|
||||||
];
|
|
||||||
if marker_end < line.len() {
|
|
||||||
let content = &line[marker_end..];
|
|
||||||
let inner = parse_inline(content, marker_end);
|
|
||||||
if inner.is_empty() {
|
|
||||||
return spans;
|
|
||||||
}
|
|
||||||
spans.extend(inner);
|
|
||||||
}
|
|
||||||
return spans;
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_inline(line, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn heading_level(trimmed: &str) -> Option<usize> {
|
|
||||||
let bytes = trimmed.as_bytes();
|
|
||||||
if bytes.is_empty() || bytes[0] != b'#' { return None; }
|
|
||||||
let mut level = 0;
|
|
||||||
while level < bytes.len() && bytes[level] == b'#' { level += 1; }
|
|
||||||
if level > 3 { return None; }
|
|
||||||
if level < bytes.len() && bytes[level] == b' ' {
|
|
||||||
Some(level)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_horizontal_rule(trimmed: &str) -> bool {
|
|
||||||
if trimmed.len() < 3 { return false; }
|
|
||||||
let first = trimmed.as_bytes()[0];
|
|
||||||
if !matches!(first, b'-' | b'*' | b'_') { return false; }
|
|
||||||
trimmed.bytes().all(|b| b == first || b == b' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
enum ListKind {
|
|
||||||
Plain(usize),
|
|
||||||
TaskOpen(usize),
|
|
||||||
TaskDone(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list_marker_info(trimmed: &str) -> Option<ListKind> {
|
|
||||||
let bytes = trimmed.as_bytes();
|
|
||||||
if bytes.is_empty() { return None; }
|
|
||||||
|
|
||||||
if matches!(bytes[0], b'-' | b'*' | b'+') && bytes.get(1) == Some(&b' ') {
|
|
||||||
if trimmed.starts_with("- [ ] ") {
|
|
||||||
return Some(ListKind::TaskOpen(6));
|
|
||||||
}
|
|
||||||
if trimmed.starts_with("- [x] ") || trimmed.starts_with("- [X] ") {
|
|
||||||
return Some(ListKind::TaskDone(6));
|
|
||||||
}
|
|
||||||
return Some(ListKind::Plain(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; }
|
|
||||||
if i > 0 && i < bytes.len() && matches!(bytes[i], b'.' | b')') {
|
|
||||||
if bytes.get(i + 1) == Some(&b' ') {
|
|
||||||
return Some(ListKind::Plain(i + 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_inline(text: &str, base: usize) -> Vec<(Range<usize>, SyntaxHighlight)> {
|
|
||||||
let bytes = text.as_bytes();
|
|
||||||
let len = bytes.len();
|
|
||||||
let mut spans = Vec::new();
|
|
||||||
let mut i = 0;
|
|
||||||
|
|
||||||
while i < len {
|
|
||||||
if bytes[i] == b'\\' && i + 1 < len && is_md_punctuation(bytes[i + 1]) {
|
|
||||||
i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if i + 2 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' && bytes[i + 2] == b'*' {
|
|
||||||
if let Some(end) = find_triple_star(bytes, i + 3) {
|
|
||||||
spans.push((base + i..base + i + 3, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
if i + 3 < end {
|
|
||||||
spans.push((base + i + 3..base + end, SyntaxHighlight { kind: MD_BOLD_ITALIC }));
|
|
||||||
}
|
|
||||||
spans.push((base + end..base + end + 3, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
i = end + 3;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' {
|
|
||||||
if let Some(end) = find_closing(bytes, i + 2, b'*', b'*') {
|
|
||||||
spans.push((base + i..base + i + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
if i + 2 < end {
|
|
||||||
let inner = parse_inline(&text[i + 2..end], base + i + 2);
|
|
||||||
if inner.is_empty() {
|
|
||||||
spans.push((base + i + 2..base + end, SyntaxHighlight { kind: MD_BOLD }));
|
|
||||||
} else {
|
|
||||||
for (r, h) in inner {
|
|
||||||
let kind = if h.kind == MD_ITALIC { MD_BOLD_ITALIC } else { h.kind };
|
|
||||||
spans.push((r, SyntaxHighlight { kind }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spans.push((base + end..base + end + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
i = end + 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes[i] == b'*' && (i + 1 >= len || bytes[i + 1] != b'*') {
|
|
||||||
if let Some(end) = find_single_closing(bytes, i + 1, b'*') {
|
|
||||||
if end > i + 1 && bytes[end - 1] != b'*' {
|
|
||||||
spans.push((base + i..base + i + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
if i + 1 < end {
|
|
||||||
spans.push((base + i + 1..base + end, SyntaxHighlight { kind: MD_ITALIC }));
|
|
||||||
}
|
|
||||||
spans.push((base + end..base + end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
i = end + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes[i] == b'`' {
|
|
||||||
let tick_count = count_backticks(bytes, i);
|
|
||||||
if let Some(end) = find_backtick_close(bytes, i + tick_count, tick_count) {
|
|
||||||
spans.push((base + i..base + i + tick_count, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
if i + tick_count < end {
|
|
||||||
spans.push((base + i + tick_count..base + end, SyntaxHighlight { kind: MD_INLINE_CODE }));
|
|
||||||
}
|
|
||||||
spans.push((base + end..base + end + tick_count, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
i = end + tick_count;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes[i] == b'[' {
|
|
||||||
if let Some((text_end, url_end)) = find_link(bytes, i) {
|
|
||||||
spans.push((base + i..base + i + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
if i + 1 < text_end {
|
|
||||||
spans.push((base + i + 1..base + text_end, SyntaxHighlight { kind: MD_LINK_TEXT }));
|
|
||||||
}
|
|
||||||
spans.push((base + text_end..base + text_end + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
if text_end + 2 < url_end {
|
|
||||||
spans.push((base + text_end + 2..base + url_end, SyntaxHighlight { kind: MD_LINK_URL }));
|
|
||||||
}
|
|
||||||
spans.push((base + url_end..base + url_end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
|
||||||
i = url_end + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
spans
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_md_punctuation(b: u8) -> bool {
|
|
||||||
matches!(b, b'\\' | b'`' | b'*' | b'_' | b'{' | b'}' | b'[' | b']'
|
|
||||||
| b'(' | b')' | b'#' | b'+' | b'-' | b'.' | b'!' | b'|')
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_triple_star(bytes: &[u8], start: usize) -> Option<usize> {
|
|
||||||
let mut i = start;
|
|
||||||
while i + 2 < bytes.len() {
|
|
||||||
if bytes[i] == b'*' && bytes[i + 1] == b'*' && bytes[i + 2] == b'*' {
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_backticks(bytes: &[u8], start: usize) -> usize {
|
|
||||||
let mut n = 0;
|
|
||||||
while start + n < bytes.len() && bytes[start + n] == b'`' { n += 1; }
|
|
||||||
n
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_backtick_close(bytes: &[u8], start: usize, count: usize) -> Option<usize> {
|
|
||||||
if count == 0 { return None; }
|
|
||||||
let mut i = start;
|
|
||||||
while i + count <= bytes.len() {
|
|
||||||
if count_backticks(bytes, i) == count {
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_closing(bytes: &[u8], start: usize, c1: u8, c2: u8) -> Option<usize> {
|
|
||||||
let mut i = start;
|
|
||||||
while i + 1 < bytes.len() {
|
|
||||||
if bytes[i] == c1 && bytes[i + 1] == c2 {
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_single_closing(bytes: &[u8], start: usize, ch: u8) -> Option<usize> {
|
|
||||||
let mut i = start;
|
|
||||||
while i < bytes.len() {
|
|
||||||
if bytes[i] == ch {
|
|
||||||
return Some(i);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_link(bytes: &[u8], open: usize) -> Option<(usize, usize)> {
|
|
||||||
let mut i = open + 1;
|
|
||||||
while i < bytes.len() {
|
|
||||||
if bytes[i] == b']' {
|
|
||||||
if i + 1 < bytes.len() && bytes[i + 1] == b'(' {
|
|
||||||
let text_end = i;
|
|
||||||
let mut j = i + 2;
|
|
||||||
while j < bytes.len() {
|
|
||||||
if bytes[j] == b')' {
|
|
||||||
return Some((text_end, j));
|
|
||||||
}
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if bytes[i] == b'\n' { return None; }
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl highlighter::Highlighter for SyntaxHighlighter {
|
impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
|
|
@ -399,10 +49,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
lang: settings.lang.clone(),
|
lang: settings.lang.clone(),
|
||||||
spans: Vec::new(),
|
spans: Vec::new(),
|
||||||
line_offsets: Vec::new(),
|
line_offsets: Vec::new(),
|
||||||
line_kinds: Vec::new(),
|
|
||||||
in_fenced_code: false,
|
|
||||||
current_line: 0,
|
current_line: 0,
|
||||||
line_decors: Vec::new(),
|
|
||||||
};
|
};
|
||||||
h.rebuild(&settings.source);
|
h.rebuild(&settings.source);
|
||||||
h
|
h
|
||||||
|
|
@ -415,42 +62,18 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
|
|
||||||
fn change_line(&mut self, line: usize) {
|
fn change_line(&mut self, line: usize) {
|
||||||
self.current_line = self.current_line.min(line);
|
self.current_line = self.current_line.min(line);
|
||||||
if line == 0 {
|
|
||||||
self.in_fenced_code = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
fn highlight_line(&mut self, _line: &str) -> Self::Iterator<'_> {
|
||||||
let ln = self.current_line;
|
let ln = self.current_line;
|
||||||
self.current_line += 1;
|
self.current_line += 1;
|
||||||
|
|
||||||
let trimmed = line.trim_start();
|
let trimmed = _line.trim_start();
|
||||||
if trimmed.starts_with(RESULT_PREFIX) {
|
if trimmed.starts_with(RESULT_PREFIX) {
|
||||||
return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter();
|
return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter();
|
||||||
}
|
}
|
||||||
if trimmed.starts_with(ERROR_PREFIX) {
|
if trimmed.starts_with(ERROR_PREFIX) {
|
||||||
return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter();
|
return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter();
|
||||||
}
|
|
||||||
|
|
||||||
let is_markdown = ln < self.line_kinds.len()
|
|
||||||
&& self.line_kinds[ln] == LineKind::Markdown;
|
|
||||||
|
|
||||||
if is_markdown {
|
|
||||||
if trimmed.starts_with("```") {
|
|
||||||
self.in_fenced_code = !self.in_fenced_code;
|
|
||||||
return vec![(0..line.len(), SyntaxHighlight { kind: MD_FENCE_MARKER })].into_iter();
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.in_fenced_code {
|
|
||||||
return vec![(0..line.len(), SyntaxHighlight { kind: MD_CODE_BLOCK })].into_iter();
|
|
||||||
}
|
|
||||||
|
|
||||||
let md_spans = self.highlight_markdown(line);
|
|
||||||
if !md_spans.is_empty() {
|
|
||||||
return md_spans.into_iter();
|
|
||||||
}
|
|
||||||
} else if self.in_fenced_code {
|
|
||||||
self.in_fenced_code = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ln >= self.line_offsets.len() {
|
if ln >= self.line_offsets.len() {
|
||||||
|
|
@ -461,7 +84,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
let line_end = if ln + 1 < self.line_offsets.len() {
|
let line_end = if ln + 1 < self.line_offsets.len() {
|
||||||
self.line_offsets[ln + 1] - 1
|
self.line_offsets[ln + 1] - 1
|
||||||
} else {
|
} else {
|
||||||
line_start + line.len()
|
line_start + _line.len()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
@ -484,101 +107,33 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_color(kind: u8) -> Color {
|
pub fn highlight_color(kind: u8) -> Color {
|
||||||
let p = palette::current();
|
|
||||||
match kind {
|
match kind {
|
||||||
0 => p.mauve,
|
0 => Color::from_rgb(0.804, 0.569, 0.945), // keyword - mauve
|
||||||
1 => p.blue,
|
1 => Color::from_rgb(0.537, 0.706, 0.980), // function - blue
|
||||||
2 => p.teal,
|
2 => Color::from_rgb(0.604, 0.831, 0.898), // function.builtin - teal
|
||||||
3 => p.yellow,
|
3 => Color::from_rgb(0.976, 0.827, 0.522), // type - yellow
|
||||||
4 => p.yellow,
|
4 => Color::from_rgb(0.976, 0.827, 0.522), // type.builtin - yellow
|
||||||
5 => p.teal,
|
5 => Color::from_rgb(0.569, 0.878, 0.800), // constructor - teal
|
||||||
6 => p.peach,
|
6 => Color::from_rgb(0.988, 0.702, 0.529), // constant - peach
|
||||||
7 => p.peach,
|
7 => Color::from_rgb(0.988, 0.702, 0.529), // constant.builtin - peach
|
||||||
8 => p.green,
|
8 => Color::from_rgb(0.651, 0.890, 0.631), // string - green
|
||||||
9 => p.peach,
|
9 => Color::from_rgb(0.988, 0.702, 0.529), // number - peach
|
||||||
10 => p.overlay0,
|
10 => Color::from_rgb(0.424, 0.443, 0.537), // comment - overlay0
|
||||||
11 => p.text,
|
11 => Color::from_rgb(0.804, 0.839, 0.957), // variable - text
|
||||||
12 => p.red,
|
12 => Color::from_rgb(0.949, 0.604, 0.584), // variable.builtin - red
|
||||||
13 => p.flamingo,
|
13 => Color::from_rgb(0.949, 0.773, 0.584), // variable.parameter - flamingo
|
||||||
14 => p.sky,
|
14 => Color::from_rgb(0.604, 0.831, 0.898), // operator - sky
|
||||||
15 => p.overlay2,
|
15 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation - overlay2
|
||||||
16 => p.overlay2,
|
16 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.bracket - overlay2
|
||||||
17 => p.overlay2,
|
17 => Color::from_rgb(0.580, 0.612, 0.733), // punctuation.delimiter - overlay2
|
||||||
18 => p.blue,
|
18 => Color::from_rgb(0.537, 0.706, 0.980), // property - blue
|
||||||
19 => p.mauve,
|
19 => Color::from_rgb(0.804, 0.569, 0.945), // tag - mauve
|
||||||
20 => p.yellow,
|
20 => Color::from_rgb(0.976, 0.827, 0.522), // attribute - yellow
|
||||||
21 => p.teal,
|
21 => Color::from_rgb(0.569, 0.878, 0.800), // label - teal
|
||||||
22 => p.red,
|
22 => Color::from_rgb(0.949, 0.604, 0.584), // escape - red
|
||||||
23 => p.text,
|
23 => Color::from_rgb(0.804, 0.839, 0.957), // embedded - text
|
||||||
24 => p.green,
|
24 => Color::from_rgb(0.651, 0.890, 0.631), // eval result - green
|
||||||
25 => p.maroon,
|
25 => Color::from_rgb(0.890, 0.400, 0.400), // eval error - muted red
|
||||||
MD_HEADING_MARKER => p.overlay0,
|
_ => Color::from_rgb(0.804, 0.839, 0.957), // default text
|
||||||
MD_H1 => p.rosewater,
|
|
||||||
MD_H2 => p.peach,
|
|
||||||
MD_H3 => p.yellow,
|
|
||||||
MD_BOLD => p.peach,
|
|
||||||
MD_ITALIC => p.mauve,
|
|
||||||
MD_INLINE_CODE => p.green,
|
|
||||||
MD_FORMAT_MARKER => p.overlay0,
|
|
||||||
MD_LINK_TEXT => p.blue,
|
|
||||||
MD_LINK_URL => p.overlay1,
|
|
||||||
MD_BLOCKQUOTE_MARKER => p.overlay0,
|
|
||||||
MD_BLOCKQUOTE => p.sky,
|
|
||||||
MD_LIST_MARKER => p.sky,
|
|
||||||
MD_FENCE_MARKER => p.overlay0,
|
|
||||||
MD_CODE_BLOCK => p.text,
|
|
||||||
MD_HR => p.overlay1,
|
|
||||||
MD_TASK_OPEN => p.overlay2,
|
|
||||||
MD_TASK_DONE => p.green,
|
|
||||||
MD_BOLD_ITALIC => p.peach,
|
|
||||||
_ => p.text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_font(kind: u8) -> Option<Font> {
|
|
||||||
match kind {
|
|
||||||
MD_HEADING_MARKER => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }),
|
|
||||||
MD_H1 => Some(Font { weight: Weight::Black, ..Font::DEFAULT }),
|
|
||||||
MD_H2 => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }),
|
|
||||||
MD_H3 => Some(Font { weight: Weight::Semibold, ..Font::DEFAULT }),
|
|
||||||
MD_BOLD => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }),
|
|
||||||
MD_ITALIC => Some(Font { style: FontStyle::Italic, ..Font::DEFAULT }),
|
|
||||||
MD_BOLD_ITALIC => Some(Font { weight: Weight::Bold, style: FontStyle::Italic, ..Font::DEFAULT }),
|
|
||||||
MD_INLINE_CODE => Some(Font::MONOSPACE),
|
|
||||||
MD_FORMAT_MARKER => Some(Font::MONOSPACE),
|
|
||||||
MD_BLOCKQUOTE => Some(Font { style: FontStyle::Italic, ..Font::DEFAULT }),
|
|
||||||
MD_FENCE_MARKER => Some(Font::MONOSPACE),
|
|
||||||
MD_CODE_BLOCK => Some(Font::MONOSPACE),
|
|
||||||
MD_TASK_DONE => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_line_decors(source: &str) -> Vec<LineDecor> {
|
|
||||||
let classified = classify_document(source);
|
|
||||||
let line_kinds: Vec<LineKind> = classified.into_iter().map(|cl| cl.kind).collect();
|
|
||||||
let mut decors = Vec::new();
|
|
||||||
let mut in_fence = false;
|
|
||||||
for (i, raw_line) in source.split('\n').enumerate() {
|
|
||||||
let is_md = i < line_kinds.len() && line_kinds[i] == LineKind::Markdown;
|
|
||||||
if is_md {
|
|
||||||
let trimmed = raw_line.trim_start();
|
|
||||||
if trimmed.starts_with("```") {
|
|
||||||
in_fence = !in_fence;
|
|
||||||
decors.push(LineDecor::FenceMarker);
|
|
||||||
} else if in_fence {
|
|
||||||
decors.push(LineDecor::CodeBlock);
|
|
||||||
} else if is_horizontal_rule(trimmed) {
|
|
||||||
decors.push(LineDecor::HorizontalRule);
|
|
||||||
} else if trimmed.starts_with("> ") || trimmed == ">" {
|
|
||||||
decors.push(LineDecor::Blockquote);
|
|
||||||
} else {
|
|
||||||
decors.push(LineDecor::None);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if in_fence { in_fence = false; }
|
|
||||||
decors.push(LineDecor::None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decors
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,329 +0,0 @@
|
||||||
use iced_wgpu::core::widget::Id as WidgetId;
|
|
||||||
use iced_wgpu::core::{
|
|
||||||
Background, Border, Color, Element, Font, Length,
|
|
||||||
Padding, Shadow, Theme,
|
|
||||||
};
|
|
||||||
use iced_widget::container;
|
|
||||||
use iced_widget::text_input;
|
|
||||||
|
|
||||||
use crate::palette;
|
|
||||||
|
|
||||||
const MIN_COL_WIDTH: f32 = 60.0;
|
|
||||||
const DEFAULT_COL_WIDTH: f32 = 120.0;
|
|
||||||
const CELL_PADDING: Padding = Padding {
|
|
||||||
top: 3.0,
|
|
||||||
right: 6.0,
|
|
||||||
bottom: 3.0,
|
|
||||||
left: 6.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum TableMessage {
|
|
||||||
CellChanged(usize, usize, String),
|
|
||||||
FocusCell(usize, usize),
|
|
||||||
AddRow,
|
|
||||||
AddColumn,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TableState {
|
|
||||||
pub rows: Vec<Vec<String>>,
|
|
||||||
pub col_widths: Vec<f32>,
|
|
||||||
pub focused_cell: Option<(usize, usize)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TableState {
|
|
||||||
pub fn new(rows: Vec<Vec<String>>, col_widths: Option<Vec<f32>>) -> Self {
|
|
||||||
let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
||||||
let widths = col_widths.unwrap_or_else(|| vec![DEFAULT_COL_WIDTH; col_count]);
|
|
||||||
Self {
|
|
||||||
rows,
|
|
||||||
col_widths: widths,
|
|
||||||
focused_cell: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_markdown(text: &str) -> Option<Self> {
|
|
||||||
let lines: Vec<&str> = text.lines().collect();
|
|
||||||
if lines.len() < 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parse_row = |line: &str| -> Vec<String> {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
let inner = trimmed.strip_prefix('|').unwrap_or(trimmed);
|
|
||||||
let inner = inner.strip_suffix('|').unwrap_or(inner);
|
|
||||||
inner.split('|').map(|c| c.trim().to_string()).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_separator = |line: &str| -> bool {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
let inner = trimmed.strip_prefix('|').unwrap_or(trimmed);
|
|
||||||
let inner = inner.strip_suffix('|').unwrap_or(inner);
|
|
||||||
inner.split('|').all(|seg| {
|
|
||||||
let s = seg.trim();
|
|
||||||
s.chars().all(|c| c == '-' || c == ':') && s.len() >= 1
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = parse_row(lines[0]);
|
|
||||||
if !is_separator(lines[1]) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rows = vec![header];
|
|
||||||
for line in &lines[2..] {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
if trimmed.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rows.push(parse_row(trimmed));
|
|
||||||
}
|
|
||||||
|
|
||||||
let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
||||||
for row in &mut rows {
|
|
||||||
while row.len() < col_count {
|
|
||||||
row.push(String::new());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let widths: Vec<f32> = (0..col_count)
|
|
||||||
.map(|ci| {
|
|
||||||
let max_len = rows.iter()
|
|
||||||
.map(|r| r.get(ci).map(|s| s.len()).unwrap_or(0))
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
((max_len as f32) * 9.0).max(MIN_COL_WIDTH).min(300.0)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(Self::new(rows, Some(widths)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_eval_rows(data: Vec<Vec<String>>) -> Self {
|
|
||||||
let col_count = data.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
||||||
let widths: Vec<f32> = (0..col_count)
|
|
||||||
.map(|ci| {
|
|
||||||
let max_len = data.iter()
|
|
||||||
.map(|r| r.get(ci).map(|s| s.len()).unwrap_or(0))
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
((max_len as f32) * 9.0).max(MIN_COL_WIDTH).min(300.0)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Self::new(data, Some(widths))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn col_count(&self) -> usize {
|
|
||||||
self.col_widths.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn row_count(&self) -> usize {
|
|
||||||
self.rows.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, msg: TableMessage) {
|
|
||||||
match msg {
|
|
||||||
TableMessage::CellChanged(row, col, val) => {
|
|
||||||
if row < self.rows.len() && col < self.rows[row].len() {
|
|
||||||
self.rows[row][col] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TableMessage::FocusCell(row, col) => {
|
|
||||||
self.focused_cell = Some((row, col));
|
|
||||||
}
|
|
||||||
TableMessage::AddRow => {
|
|
||||||
let cols = self.col_count();
|
|
||||||
self.rows.push(vec![String::new(); cols]);
|
|
||||||
}
|
|
||||||
TableMessage::AddColumn => {
|
|
||||||
let new_width = DEFAULT_COL_WIDTH;
|
|
||||||
self.col_widths.push(new_width);
|
|
||||||
for row in &mut self.rows {
|
|
||||||
row.push(String::new());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_cell(&self, row: usize, col: usize) -> Option<(usize, usize)> {
|
|
||||||
let cols = self.col_count();
|
|
||||||
let rows = self.row_count();
|
|
||||||
if col + 1 < cols {
|
|
||||||
Some((row, col + 1))
|
|
||||||
} else if row + 1 < rows {
|
|
||||||
Some((row + 1, 0))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_cell(&self, row: usize, col: usize) -> Option<(usize, usize)> {
|
|
||||||
let cols = self.col_count();
|
|
||||||
if col > 0 {
|
|
||||||
Some((row, col - 1))
|
|
||||||
} else if row > 0 {
|
|
||||||
Some((row - 1, cols.saturating_sub(1)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cell_below(&self, row: usize, col: usize) -> Option<(usize, usize)> {
|
|
||||||
if row + 1 < self.row_count() {
|
|
||||||
Some((row + 1, col))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_markdown(&self) -> String {
|
|
||||||
if self.rows.is_empty() {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let col_count = self.col_count();
|
|
||||||
let mut widths = vec![0usize; col_count];
|
|
||||||
for row in &self.rows {
|
|
||||||
for (i, cell) in row.iter().enumerate() {
|
|
||||||
if i < col_count {
|
|
||||||
widths[i] = widths[i].max(cell.len()).max(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = String::new();
|
|
||||||
|
|
||||||
// header
|
|
||||||
out.push('|');
|
|
||||||
if let Some(header) = self.rows.first() {
|
|
||||||
for (i, cell) in header.iter().enumerate() {
|
|
||||||
let w = if i < widths.len() { widths[i] } else { 3 };
|
|
||||||
out.push_str(&format!(" {:width$} |", cell, width = w));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push('\n');
|
|
||||||
|
|
||||||
// separator
|
|
||||||
out.push('|');
|
|
||||||
for w in &widths {
|
|
||||||
out.push_str(&format!("-{}-|", "-".repeat(*w)));
|
|
||||||
}
|
|
||||||
out.push('\n');
|
|
||||||
|
|
||||||
// data rows
|
|
||||||
for row in self.rows.iter().skip(1) {
|
|
||||||
out.push('|');
|
|
||||||
for (i, cell) in row.iter().enumerate() {
|
|
||||||
let w = if i < widths.len() { widths[i] } else { 3 };
|
|
||||||
out.push_str(&format!(" {:width$} |", cell, width = w));
|
|
||||||
}
|
|
||||||
out.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pending_focus_id(&self) -> Option<WidgetId> {
|
|
||||||
self.focused_cell
|
|
||||||
.map(|(r, c)| WidgetId::from(format!("table_cell_{}_{}", r, c)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cell_id(row: usize, col: usize) -> WidgetId {
|
|
||||||
WidgetId::from(format!("table_cell_{}_{}", row, col))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cell_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
|
|
||||||
let p = palette::current();
|
|
||||||
text_input::Style {
|
|
||||||
background: Background::Color(p.surface0),
|
|
||||||
border: Border {
|
|
||||||
color: p.surface2,
|
|
||||||
width: 1.0,
|
|
||||||
radius: 0.0.into(),
|
|
||||||
},
|
|
||||||
icon: p.overlay2,
|
|
||||||
placeholder: p.overlay0,
|
|
||||||
value: p.text,
|
|
||||||
selection: Color { a: 0.4, ..p.blue },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn header_cell_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
|
|
||||||
let p = palette::current();
|
|
||||||
text_input::Style {
|
|
||||||
background: Background::Color(p.mantle),
|
|
||||||
border: Border {
|
|
||||||
color: p.surface2,
|
|
||||||
width: 1.0,
|
|
||||||
radius: 0.0.into(),
|
|
||||||
},
|
|
||||||
icon: p.overlay2,
|
|
||||||
placeholder: p.overlay0,
|
|
||||||
value: p.text,
|
|
||||||
selection: Color { a: 0.4, ..p.blue },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn table_view<'a, Message, F>(
|
|
||||||
state: &'a TableState,
|
|
||||||
on_msg: F,
|
|
||||||
) -> Element<'a, Message, Theme, iced_wgpu::Renderer>
|
|
||||||
where
|
|
||||||
Message: Clone + 'a,
|
|
||||||
F: Fn(TableMessage) -> Message + 'a + Copy,
|
|
||||||
{
|
|
||||||
let mut col_elements: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
|
||||||
|
|
||||||
for (ri, row) in state.rows.iter().enumerate() {
|
|
||||||
let is_header = ri == 0;
|
|
||||||
let mut row_cells: Vec<Element<'a, Message, Theme, iced_wgpu::Renderer>> = Vec::new();
|
|
||||||
|
|
||||||
for (ci, cell) in row.iter().enumerate() {
|
|
||||||
let width = state.col_widths.get(ci).copied().unwrap_or(DEFAULT_COL_WIDTH);
|
|
||||||
let r = ri;
|
|
||||||
let c = ci;
|
|
||||||
|
|
||||||
let style_fn: fn(&Theme, text_input::Status) -> text_input::Style = if is_header {
|
|
||||||
header_cell_style
|
|
||||||
} else {
|
|
||||||
cell_input_style
|
|
||||||
};
|
|
||||||
|
|
||||||
let font = if is_header {
|
|
||||||
Font { weight: iced_wgpu::core::font::Weight::Bold, ..Font::MONOSPACE }
|
|
||||||
} else {
|
|
||||||
Font::MONOSPACE
|
|
||||||
};
|
|
||||||
|
|
||||||
let input = text_input::TextInput::new("", cell)
|
|
||||||
.on_input(move |val| on_msg(TableMessage::CellChanged(r, c, val)))
|
|
||||||
.id(cell_id(ri, ci))
|
|
||||||
.font(font)
|
|
||||||
.size(13.0)
|
|
||||||
.padding(CELL_PADDING)
|
|
||||||
.width(Length::Fixed(width))
|
|
||||||
.style(style_fn);
|
|
||||||
|
|
||||||
row_cells.push(input.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let row_el: Element<'a, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
iced_widget::row(row_cells).spacing(0.0).into();
|
|
||||||
col_elements.push(row_el);
|
|
||||||
}
|
|
||||||
|
|
||||||
let table: Element<'a, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
iced_widget::column(col_elements).spacing(0.0).into();
|
|
||||||
|
|
||||||
iced_widget::container(table)
|
|
||||||
.padding(Padding { top: 4.0, right: 0.0, bottom: 4.0, left: 8.0 })
|
|
||||||
.style(|_theme: &Theme| container::Style {
|
|
||||||
background: None,
|
|
||||||
border: Border::default(),
|
|
||||||
text_color: None,
|
|
||||||
shadow: Shadow::default(),
|
|
||||||
snap: false,
|
|
||||||
})
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
use iced_wgpu::core::text::LineHeight;
|
|
||||||
use iced_wgpu::core::{
|
|
||||||
alignment, Element, Font, Length, Pixels, Point, Rectangle, Theme,
|
|
||||||
};
|
|
||||||
use iced_widget::canvas;
|
|
||||||
|
|
||||||
use crate::palette;
|
|
||||||
|
|
||||||
const NODE_HEIGHT: f32 = 20.0;
|
|
||||||
const INDENT_PX: f32 = 20.0;
|
|
||||||
const FONT_SIZE: f32 = 13.0;
|
|
||||||
const BRANCH_INSET: f32 = 12.0;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum TreeMessage {}
|
|
||||||
|
|
||||||
struct TreeNode {
|
|
||||||
label: String,
|
|
||||||
depth: usize,
|
|
||||||
is_last: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flatten_tree(val: &serde_json::Value, depth: usize, is_last: bool, out: &mut Vec<TreeNode>) {
|
|
||||||
match val {
|
|
||||||
serde_json::Value::Array(items) => {
|
|
||||||
if depth > 0 {
|
|
||||||
out.push(TreeNode {
|
|
||||||
label: "[array]".into(),
|
|
||||||
depth,
|
|
||||||
is_last,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let len = items.len();
|
|
||||||
for (i, item) in items.iter().enumerate() {
|
|
||||||
flatten_tree(item, depth + 1, i == len - 1, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::Value::Object(_) => {
|
|
||||||
out.push(TreeNode {
|
|
||||||
label: "{object}".into(),
|
|
||||||
depth,
|
|
||||||
is_last,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
serde_json::Value::String(s) => {
|
|
||||||
out.push(TreeNode {
|
|
||||||
label: format!("\"{}\"", s),
|
|
||||||
depth,
|
|
||||||
is_last,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
serde_json::Value::Number(n) => {
|
|
||||||
out.push(TreeNode {
|
|
||||||
label: n.to_string(),
|
|
||||||
depth,
|
|
||||||
is_last,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
serde_json::Value::Bool(b) => {
|
|
||||||
out.push(TreeNode {
|
|
||||||
label: b.to_string(),
|
|
||||||
depth,
|
|
||||||
is_last,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
serde_json::Value::Null => {
|
|
||||||
out.push(TreeNode {
|
|
||||||
label: "null".into(),
|
|
||||||
depth,
|
|
||||||
is_last,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TreeProgram {
|
|
||||||
nodes: Vec<TreeNode>,
|
|
||||||
total_height: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TreeProgram {
|
|
||||||
pub fn from_json(val: &serde_json::Value) -> Self {
|
|
||||||
let mut nodes = Vec::new();
|
|
||||||
match val {
|
|
||||||
serde_json::Value::Array(items) => {
|
|
||||||
let len = items.len();
|
|
||||||
for (i, item) in items.iter().enumerate() {
|
|
||||||
flatten_tree(item, 0, i == len - 1, &mut nodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
flatten_tree(val, 0, true, &mut nodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let total_height = (nodes.len() as f32 * NODE_HEIGHT).max(NODE_HEIGHT);
|
|
||||||
Self { nodes, total_height }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn height(&self) -> f32 {
|
|
||||||
self.total_height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Message: Clone> canvas::Program<Message, Theme, iced_wgpu::Renderer> for TreeProgram {
|
|
||||||
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 p = palette::current();
|
|
||||||
let connector_color = p.surface2;
|
|
||||||
let label_color = p.text;
|
|
||||||
let array_color = p.overlay1;
|
|
||||||
|
|
||||||
for (i, node) in self.nodes.iter().enumerate() {
|
|
||||||
let y = i as f32 * NODE_HEIGHT;
|
|
||||||
let indent_x = node.depth as f32 * INDENT_PX + 8.0;
|
|
||||||
|
|
||||||
// vertical connector from parent
|
|
||||||
if node.depth > 0 {
|
|
||||||
let parent_x = (node.depth - 1) as f32 * INDENT_PX + 8.0;
|
|
||||||
let connector = canvas::Path::new(|b| {
|
|
||||||
b.move_to(Point::new(parent_x, y));
|
|
||||||
b.line_to(Point::new(parent_x, y + NODE_HEIGHT / 2.0));
|
|
||||||
b.line_to(Point::new(parent_x + BRANCH_INSET, y + NODE_HEIGHT / 2.0));
|
|
||||||
});
|
|
||||||
frame.stroke(
|
|
||||||
&connector,
|
|
||||||
canvas::Stroke::default()
|
|
||||||
.with_width(1.0)
|
|
||||||
.with_color(connector_color),
|
|
||||||
);
|
|
||||||
|
|
||||||
// extend vertical line upward to connect siblings
|
|
||||||
if !node.is_last {
|
|
||||||
let vert = canvas::Path::line(
|
|
||||||
Point::new(indent_x - INDENT_PX, y + NODE_HEIGHT / 2.0),
|
|
||||||
Point::new(indent_x - INDENT_PX, y + NODE_HEIGHT),
|
|
||||||
);
|
|
||||||
frame.stroke(
|
|
||||||
&vert,
|
|
||||||
canvas::Stroke::default()
|
|
||||||
.with_width(1.0)
|
|
||||||
.with_color(connector_color),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let text_color = if node.label.starts_with('[') || node.label.starts_with('{') {
|
|
||||||
array_color
|
|
||||||
} else {
|
|
||||||
label_color
|
|
||||||
};
|
|
||||||
|
|
||||||
let branch_char = if node.depth == 0 {
|
|
||||||
String::new()
|
|
||||||
} else if node.is_last {
|
|
||||||
"\u{2514}\u{2500} ".into() // └─
|
|
||||||
} else {
|
|
||||||
"\u{251C}\u{2500} ".into() // ├─
|
|
||||||
};
|
|
||||||
|
|
||||||
let display = format!("{}{}", branch_char, node.label);
|
|
||||||
|
|
||||||
frame.fill_text(canvas::Text {
|
|
||||||
content: display,
|
|
||||||
position: Point::new(indent_x, y + 2.0),
|
|
||||||
max_width: bounds.width - indent_x,
|
|
||||||
color: text_color,
|
|
||||||
size: Pixels(FONT_SIZE),
|
|
||||||
line_height: LineHeight::Relative(1.3),
|
|
||||||
font: Font::MONOSPACE,
|
|
||||||
align_x: alignment::Horizontal::Left.into(),
|
|
||||||
align_y: alignment::Vertical::Top,
|
|
||||||
shaping: iced_wgpu::core::text::Shaping::Basic,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![frame.into_geometry()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view<'a, Message: Clone + 'a>(
|
|
||||||
data: &serde_json::Value,
|
|
||||||
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
|
||||||
let program = TreeProgram::from_json(data);
|
|
||||||
let h = program.height();
|
|
||||||
canvas::Canvas::new(program)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(h))
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue