forked from jess/Acord
1
0
Fork 0

Colors... We'll need a new space.

This commit is contained in:
jess 2026-04-16 02:01:36 -07:00
parent 931b8bf0b6
commit ccffb9149d
4 changed files with 221 additions and 12 deletions

View File

@ -589,12 +589,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
let mode = ConfigManager.shared.themeMode let mode = ConfigManager.shared.themeMode
let name: String let name: String
switch mode { switch mode {
case "dark": name = "mocha" case "dark": name = "kicad"
case "light": name = "latte" case "light": name = "latte"
default: default:
let appearance = NSApp.effectiveAppearance let appearance = NSApp.effectiveAppearance
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
name = isDark ? "mocha" : "latte" name = isDark ? "kicad" : "latte"
} }
viewport?.setTheme(name) viewport?.setTheme(name)
} }

View File

@ -60,6 +60,35 @@ struct Theme {
rosewater: NSColor(red: 0.961, green: 0.761, blue: 0.765, alpha: 1) rosewater: NSColor(red: 0.961, green: 0.761, blue: 0.765, alpha: 1)
) )
static let kicad = CatppuccinPalette(
base: NSColor(red: 0.090, green: 0.094, blue: 0.114, alpha: 1),
mantle: NSColor(red: 0.075, green: 0.078, blue: 0.102, alpha: 1),
crust: NSColor(red: 0.059, green: 0.059, blue: 0.059, alpha: 1),
surface0: NSColor(red: 0.102, green: 0.110, blue: 0.125, alpha: 1),
surface1: NSColor(red: 0.122, green: 0.126, blue: 0.141, alpha: 1),
surface2: NSColor(red: 0.133, green: 0.141, blue: 0.149, alpha: 1),
overlay0: NSColor(red: 0.361, green: 0.368, blue: 0.418, alpha: 1),
overlay1: NSColor(red: 0.449, green: 0.453, blue: 0.499, alpha: 1),
overlay2: NSColor(red: 0.548, green: 0.545, blue: 0.598, alpha: 1),
text: NSColor(red: 0.965, green: 0.954, blue: 0.969, alpha: 1),
subtext0: NSColor(red: 0.679, green: 0.668, blue: 0.725, alpha: 1),
subtext1: NSColor(red: 0.824, green: 0.813, blue: 0.852, alpha: 1),
red: NSColor(red: 0.914, green: 0.376, blue: 0.376, alpha: 1),
maroon: NSColor(red: 0.949, green: 0.416, blue: 0.584, alpha: 1),
peach: NSColor(red: 0.965, green: 0.533, blue: 0.404, alpha: 1),
yellow: NSColor(red: 0.988, green: 0.831, blue: 0.349, alpha: 1),
green: NSColor(red: 0.403, green: 0.972, blue: 0.534, alpha: 1),
teal: NSColor(red: 0.310, green: 1.000, blue: 0.882, alpha: 1),
sky: NSColor(red: 0.403, green: 0.813, blue: 0.972, alpha: 1),
sapphire: NSColor(red: 0.384, green: 0.635, blue: 0.949, alpha: 1),
blue: NSColor(red: 0.337, green: 0.475, blue: 0.988, alpha: 1),
lavender: NSColor(red: 1.000, green: 0.718, blue: 0.937, alpha: 1),
mauve: NSColor(red: 0.635, green: 0.282, blue: 0.980, alpha: 1),
pink: NSColor(red: 0.973, green: 0.345, blue: 0.718, alpha: 1),
flamingo: NSColor(red: 0.965, green: 0.533, blue: 0.404, alpha: 1),
rosewater: NSColor(red: 0.984, green: 0.639, blue: 0.757, alpha: 1)
)
static let latte = CatppuccinPalette( static let latte = CatppuccinPalette(
base: NSColor(red: 0.937, green: 0.929, blue: 0.961, alpha: 1), base: NSColor(red: 0.937, green: 0.929, blue: 0.961, alpha: 1),
mantle: NSColor(red: 0.906, green: 0.898, blue: 0.941, alpha: 1), mantle: NSColor(red: 0.906, green: 0.898, blue: 0.941, alpha: 1),
@ -92,12 +121,12 @@ struct Theme {
static var current: CatppuccinPalette { static var current: CatppuccinPalette {
let mode = ConfigManager.shared.themeMode let mode = ConfigManager.shared.themeMode
switch mode { switch mode {
case "dark": return mocha case "dark": return kicad
case "light": return latte case "light": return latte
default: default:
let appearance = NSApp.effectiveAppearance let appearance = NSApp.effectiveAppearance
let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua let isDark = appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
return isDark ? mocha : latte return isDark ? kicad : latte
} }
} }

View File

@ -60,6 +60,38 @@ pub static MOCHA: Palette = Palette {
crust: Color::from_rgb(0.067, 0.067, 0.106), crust: Color::from_rgb(0.067, 0.067, 0.106),
}; };
/// KiCad-inspired dark — near-black background, saturated accents, high
/// contrast. The signature KiCad schematic-editor feel: vivid greens,
/// bright cyans, punchy reds and yellows on a deep navy base.
pub static KICAD: Palette = Palette {
rosewater: Color::from_rgb(0.984, 0.639, 0.757),
flamingo: Color::from_rgb(0.965, 0.533, 0.404),
pink: Color::from_rgb(0.973, 0.345, 0.718),
mauve: Color::from_rgb(0.635, 0.282, 0.980),
red: Color::from_rgb(0.914, 0.376, 0.376),
maroon: Color::from_rgb(0.949, 0.416, 0.584),
peach: Color::from_rgb(0.965, 0.533, 0.404),
yellow: Color::from_rgb(0.988, 0.831, 0.349),
green: Color::from_rgb(0.403, 0.972, 0.534),
teal: Color::from_rgb(0.310, 1.000, 0.882),
sky: Color::from_rgb(0.403, 0.813, 0.972),
sapphire: Color::from_rgb(0.384, 0.635, 0.949),
blue: Color::from_rgb(0.337, 0.475, 0.988),
lavender: Color::from_rgb(1.000, 0.718, 0.937),
text: Color::from_rgb(0.965, 0.954, 0.969),
subtext1: Color::from_rgb(0.824, 0.813, 0.852),
subtext0: Color::from_rgb(0.679, 0.668, 0.725),
overlay2: Color::from_rgb(0.548, 0.545, 0.598),
overlay1: Color::from_rgb(0.449, 0.453, 0.499),
overlay0: Color::from_rgb(0.361, 0.368, 0.418),
surface2: Color::from_rgb(0.133, 0.141, 0.149),
surface1: Color::from_rgb(0.122, 0.126, 0.141),
surface0: Color::from_rgb(0.102, 0.110, 0.125),
base: Color::from_rgb(0.090, 0.094, 0.114),
mantle: Color::from_rgb(0.075, 0.078, 0.102),
crust: Color::from_rgb(0.059, 0.059, 0.059),
};
pub static LATTE: Palette = Palette { pub static LATTE: Palette = Palette {
rosewater: Color::from_rgb(0.863, 0.541, 0.471), rosewater: Color::from_rgb(0.863, 0.541, 0.471),
flamingo: Color::from_rgb(0.867, 0.471, 0.471), flamingo: Color::from_rgb(0.867, 0.471, 0.471),
@ -105,7 +137,8 @@ pub fn is_dark() -> bool {
pub fn set_theme(name: &str) { pub fn set_theme(name: &str) {
let (pal, dark) = match name { let (pal, dark) = match name {
"latte" | "light" => (&LATTE, false), "latte" | "light" => (&LATTE, false),
_ => (&MOCHA, true), "kicad" => (&KICAD, true),
_ => (&KICAD, true),
}; };
CURRENT.with(|c| *c.borrow_mut() = pal); CURRENT.with(|c| *c.borrow_mut() = pal);
IS_DARK.with(|d| *d.borrow_mut() = dark); IS_DARK.with(|d| *d.borrow_mut() = dark);

View File

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::ops::Range; use std::ops::Range;
use iced_wgpu::core::text::highlighter; use iced_wgpu::core::text::highlighter;
@ -32,6 +33,15 @@ const COR_OPERATOR: u8 = 62;
const COR_BRACKET: u8 = 63; const COR_BRACKET: u8 = 63;
const COR_TYPE_ANN: u8 = 64; const COR_TYPE_ANN: u8 = 64;
// Per-identifier rainbow. Each user-introduced name (let, fn, params, for var,
// math-form fn def) gets one of eight palette slots, picked with a stride
// that avoids adjacent colors landing on consecutive identifiers. Subsequent
// references resolve to the same slot so the name reads the same color
// throughout the document.
const USER_IDENT_BASE: u8 = 70;
const USER_IDENT_PALETTE_SIZE: u8 = 8;
const USER_IDENT_HOP: u32 = 3;
const MD_HEADING_MARKER: u8 = 26; const MD_HEADING_MARKER: u8 = 26;
const MD_H1: u8 = 27; const MD_H1: u8 = 27;
const MD_H2: u8 = 28; const MD_H2: u8 = 28;
@ -92,6 +102,7 @@ pub struct SyntaxHighlighter {
in_fenced_code: bool, in_fenced_code: bool,
current_line: usize, current_line: usize,
line_decors: Vec<LineDecor>, line_decors: Vec<LineDecor>,
user_idents: HashMap<String, u8>,
} }
impl SyntaxHighlighter { impl SyntaxHighlighter {
@ -132,6 +143,87 @@ impl SyntaxHighlighter {
self.in_fenced_code = false; self.in_fenced_code = false;
self.current_line = 0; self.current_line = 0;
self.scan_user_idents(source);
}
/// Walk the source, find every identifier introduction site (let, fn,
/// for, math-form fn def, params), and assign each unique name a slot
/// in the user-ident rainbow. Subsequent references in `highlight_cordial`
/// look the name up here.
fn scan_user_idents(&mut self, source: &str) {
self.user_idents.clear();
let mut next_slot: u32 = 0;
for line in source.split('\n') {
let trimmed = line.trim_start();
let bytes = trimmed.as_bytes();
// `let IDENT...`
if let Some(rest) = trimmed.strip_prefix("let ") {
let mut i = 0;
let rb = rest.as_bytes();
while i < rb.len() && rb[i] == b' ' { i += 1; }
let name_start = i;
while i < rb.len() && (rb[i].is_ascii_alphanumeric() || rb[i] == b'_') { i += 1; }
if i > name_start {
assign_user_ident(&mut self.user_idents, &mut next_slot, &rest[name_start..i]);
}
while i < rb.len() && rb[i] == b' ' { i += 1; }
if i < rb.len() && rb[i] == b'(' {
extract_paren_idents(&rest[i + 1..], &mut self.user_idents, &mut next_slot);
}
continue;
}
// `fn IDENT(...)`
if let Some(rest) = trimmed.strip_prefix("fn ") {
let mut i = 0;
let rb = rest.as_bytes();
while i < rb.len() && rb[i] == b' ' { i += 1; }
let name_start = i;
while i < rb.len() && (rb[i].is_ascii_alphanumeric() || rb[i] == b'_') { i += 1; }
if i > name_start {
assign_user_ident(&mut self.user_idents, &mut next_slot, &rest[name_start..i]);
}
while i < rb.len() && rb[i] == b' ' { i += 1; }
if i < rb.len() && rb[i] == b'(' {
extract_paren_idents(&rest[i + 1..], &mut self.user_idents, &mut next_slot);
}
continue;
}
// `for IDENT in ...`
if let Some(rest) = trimmed.strip_prefix("for ") {
let rb = rest.as_bytes();
let mut i = 0;
while i < rb.len() && rb[i] == b' ' { i += 1; }
let name_start = i;
while i < rb.len() && (rb[i].is_ascii_alphanumeric() || rb[i] == b'_') { i += 1; }
if i > name_start {
assign_user_ident(&mut self.user_idents, &mut next_slot, &rest[name_start..i]);
}
continue;
}
// `IDENT(...) = ...` math-form fn def, OR `IDENT = ...` assignment
let mut i = 0;
let name_start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
if i > name_start {
let name = &trimmed[name_start..i];
let mut j = i;
while j < bytes.len() && bytes[j] == b' ' { j += 1; }
if j < bytes.len() {
if bytes[j] == b'(' {
assign_user_ident(&mut self.user_idents, &mut next_slot, name);
extract_paren_idents(&trimmed[j + 1..], &mut self.user_idents, &mut next_slot);
} else if bytes[j] == b'=' && (j + 1 >= bytes.len() || bytes[j + 1] != b'=') {
assign_user_ident(&mut self.user_idents, &mut next_slot, name);
}
}
}
}
} }
fn highlight_markdown(&self, line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> { fn highlight_markdown(&self, line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> {
@ -204,7 +296,50 @@ impl SyntaxHighlighter {
/// spans. Idempotent, single-pass; each branch either consumes a whole /// spans. Idempotent, single-pass; each branch either consumes a whole
/// token or advances one byte. Unknown bytes get no highlight (they fall /// token or advances one byte. Unknown bytes get no highlight (they fall
/// through to the editor's default text color). /// through to the editor's default text color).
fn highlight_cordial(line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> { fn assign_user_ident(map: &mut HashMap<String, u8>, slot: &mut u32, name: &str) {
if name.is_empty()
|| is_cordial_keyword(name)
|| is_cordial_builtin(name)
|| is_cordial_type_annotation(name)
|| name == "pi"
|| name == "where"
|| name == "from"
|| name == "solve"
|| map.contains_key(name)
{
return;
}
let color_idx = ((*slot * USER_IDENT_HOP) % USER_IDENT_PALETTE_SIZE as u32) as u8;
map.insert(name.to_string(), color_idx);
*slot += 1;
}
fn extract_paren_idents(s: &str, map: &mut HashMap<String, u8>, slot: &mut u32) {
let bytes = s.as_bytes();
let mut i = 0;
let mut depth: i32 = 1;
while i < bytes.len() && depth > 0 {
match bytes[i] {
b'(' => { depth += 1; i += 1; }
b')' => { depth -= 1; i += 1; }
b':' => {
// Skip the type identifier that follows; type names belong
// to the type-annotation color, not the user-ident rainbow.
i += 1;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') { i += 1; }
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
}
c if c.is_ascii_alphabetic() || c == b'_' => {
let start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; }
assign_user_ident(map, slot, &s[start..i]);
}
_ => i += 1,
}
}
}
fn highlight_cordial(line: &str, user_idents: &HashMap<String, u8>) -> Vec<(Range<usize>, SyntaxHighlight)> {
let bytes = line.as_bytes(); let bytes = line.as_bytes();
let len = bytes.len(); let len = bytes.len();
let mut spans: Vec<(Range<usize>, SyntaxHighlight)> = Vec::new(); let mut spans: Vec<(Range<usize>, SyntaxHighlight)> = Vec::new();
@ -317,9 +452,7 @@ fn highlight_cordial(line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> {
continue; continue;
} }
// Identifier → keyword / builtin / plain. Plain idents get no // Identifier → keyword / builtin / type-annotation / user-rainbow.
// highlight so user-defined names stay in the default editor
// color — keeps the document from looking like confetti.
if is_ident_byte(c) && !c.is_ascii_digit() { if is_ident_byte(c) && !c.is_ascii_digit() {
let start = i; let start = i;
while i < len && is_ident_byte(bytes[i]) { i += 1; } while i < len && is_ident_byte(bytes[i]) { i += 1; }
@ -329,9 +462,9 @@ fn highlight_cordial(line: &str) -> Vec<(Range<usize>, SyntaxHighlight)> {
} else if is_cordial_builtin(word) { } else if is_cordial_builtin(word) {
spans.push((start..i, SyntaxHighlight { kind: COR_BUILTIN_FN })); spans.push((start..i, SyntaxHighlight { kind: COR_BUILTIN_FN }));
} else if is_cordial_type_annotation(word) && last_token_is_colon(&spans) { } else if is_cordial_type_annotation(word) && last_token_is_colon(&spans) {
// Type annotation immediately after `:` in a `let x: T = …`
// reads the type name; give it the yellow/type color.
spans.push((start..i, SyntaxHighlight { kind: COR_TYPE_ANN })); spans.push((start..i, SyntaxHighlight { kind: COR_TYPE_ANN }));
} else if let Some(&slot) = user_idents.get(word) {
spans.push((start..i, SyntaxHighlight { kind: USER_IDENT_BASE + slot }));
} }
continue; continue;
} }
@ -682,6 +815,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
in_fenced_code: false, in_fenced_code: false,
current_line: 0, current_line: 0,
line_decors: Vec::new(), line_decors: Vec::new(),
user_idents: HashMap::new(),
}; };
h.rebuild(&settings.source); h.rebuild(&settings.source);
h h
@ -740,7 +874,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
if ln < self.line_kinds.len() if ln < self.line_kinds.len()
&& matches!(self.line_kinds[ln], LineKind::Cordial | LineKind::Eval | LineKind::Comment) && matches!(self.line_kinds[ln], LineKind::Cordial | LineKind::Eval | LineKind::Comment)
{ {
return highlight_cordial(line).into_iter(); return highlight_cordial(line, &self.user_idents).into_iter();
} }
if ln >= self.line_offsets.len() { if ln >= self.line_offsets.len() {
@ -775,6 +909,19 @@ impl highlighter::Highlighter for SyntaxHighlighter {
pub fn highlight_color(kind: u8) -> Color { pub fn highlight_color(kind: u8) -> Color {
let p = palette::current(); let p = palette::current();
if kind >= USER_IDENT_BASE && kind < USER_IDENT_BASE + USER_IDENT_PALETTE_SIZE {
return match kind - USER_IDENT_BASE {
0 => p.red,
1 => p.green,
2 => p.peach,
3 => p.blue,
4 => p.mauve,
5 => p.teal,
6 => p.yellow,
7 => p.pink,
_ => p.text,
};
}
match kind { match kind {
0 => p.mauve, 0 => p.mauve,
1 => p.blue, 1 => p.blue,