this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: configurable cloze highlight color; unify active/inactive rect rendering

Adds a color picker in Settings (persisted to ~/.config/tala/cloze_color)
that controls the SVG highlight color for cloze blanks and image rects in
the editor. Also collapses the duplicate active/inactive sidecar rect
rendering branches into a single loop with is_active gating interactivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+104 -45
+29 -42
crates/tala/src/editor.rs
··· 7 7 use tala_format::{CardKind}; 8 8 use tala_srs::{CardSchedule, RectEntry, Schedule, Sidecar}; 9 9 10 - use crate::util::{TagMode, card_dir, cards_path, extract_img_names}; 10 + use crate::util::{TagMode, card_dir, cards_path, cloze_color, cloze_fill_css, cloze_stroke_css, extract_img_names}; 11 11 12 12 #[derive(Clone, PartialEq)] 13 13 enum SaveStatus { ··· 1118 1118 let filter_tags_snap = editor_selected_tags.read().clone(); 1119 1119 let filter_mode_snap = editor_tag_mode.read().clone(); 1120 1120 let all_editor_tags_snap = editor_all_tags.read().clone(); 1121 + let cc = cloze_color(); 1122 + let cloze_fill = cloze_fill_css(&cc); 1123 + let cloze_stroke = cloze_stroke_css(&cc); 1121 1124 1122 1125 rsx! { 1123 1126 div { id: "editor", ··· 1270 1273 } else { 1271 1274 extract_img_names(&text_snap[seg.start..seg.end]) 1272 1275 }; 1276 + let sidecar_rect_order: Vec<usize> = if is_active { 1277 + (0..card_sidecar_rects_local.len()) 1278 + .filter(|&i| Some(i) != selected_rect) 1279 + .chain(selected_rect.into_iter().filter(|&i| i < card_sidecar_rects_local.len())) 1280 + .collect() 1281 + } else { 1282 + (0..card_sidecar_rects_local.len()).collect() 1283 + }; 1273 1284 let render_err = card_result.and_then(|r| r.err()); 1274 1285 let preview_text = text_snap[seg.start..seg.end].to_string(); 1275 1286 let frag_for_mount = active_fragment.clone(); ··· 1352 1363 rect { 1353 1364 x: "{rect[0] * iw}", y: "{rect[1] * ih}", 1354 1365 width: "{rect[2] * iw}", height: "{rect[3] * ih}", 1355 - fill: "rgba(255,220,50,0.35)", 1356 - stroke: "rgb(200,150,0)", 1366 + fill: "{cloze_fill}", 1367 + stroke: "{cloze_stroke}", 1357 1368 stroke_width: "1.5", rx: "3", 1358 1369 } 1359 1370 } 1360 1371 } 1361 - if is_active { 1372 + if seg.kind == SegKind::Card { 1362 1373 // Render selected rect last so it paints above intersecting rects. 1363 - for sri in (0..active_sidecar_rects.len()).filter(|&i| Some(i) != selected_rect).chain(selected_rect.into_iter().filter(|&i| i < active_sidecar_rects.len())) { 1374 + for sri in sidecar_rect_order.clone() { 1364 1375 { 1365 - let sr = active_sidecar_rects[sri].clone(); 1366 - let img_idx = active_img_names.iter() 1367 - .position(|n| n == &sr.src) 1368 - .unwrap_or(0); 1369 - let ib = active_img_boxes.get(img_idx).copied().unwrap_or([0.0, 0.0, 1.0, 1.0]); 1376 + let sr = card_sidecar_rects_local[sri].clone(); 1377 + let img_idx = card_img_names_local.iter().position(|n| n == &sr.src).unwrap_or(0); 1378 + let ib = card_img_boxes_local.get(img_idx).copied().unwrap_or([0.0, 0.0, 1.0, 1.0]); 1370 1379 let px = (ib[0] + sr.rect[0] as f64 * ib[2]) * iw; 1371 1380 let py = (ib[1] + sr.rect[1] as f64 * ib[3]) * ih; 1372 1381 let pw = sr.rect[2] as f64 * ib[2] * iw; 1373 1382 let ph = sr.rect[3] as f64 * ib[3] * ih; 1374 - let is_sel = selected_rect == Some(sri); 1383 + let is_sel = is_active && selected_rect == Some(sri); 1375 1384 let [bx, by, bw, bh] = ib; 1376 1385 let rid = sr.id.clone(); 1377 1386 rsx! { 1378 1387 g { key: "{rid}", 1379 - style: "pointer-events: all;", 1388 + style: if is_active { "pointer-events: all;" } else { "pointer-events: none;" }, 1380 1389 rect { 1381 1390 x: "{px}", y: "{py}", 1382 1391 width: "{pw}", height: "{ph}", 1383 - fill: "rgba(255,220,50,0.35)", 1384 - stroke: if is_sel { "rgb(60,120,220)" } else { "rgb(200,150,0)" }, 1392 + fill: "{cloze_fill}", 1393 + stroke: if is_sel { "rgb(60,120,220)" } else { "{cloze_stroke}" }, 1385 1394 stroke_width: if is_sel { "2.5" } else { "1.5" }, 1386 1395 rx: "3", 1387 - style: if is_sel { "cursor: grab;" } else { "cursor: pointer;" }, 1396 + style: if is_sel { "cursor: grab;" } else if is_active { "cursor: pointer;" } else { "" }, 1388 1397 onclick: move |e| { 1389 - e.stop_propagation(); 1390 - selected_rect_sig.set(Some(sri)); 1398 + if is_active { 1399 + e.stop_propagation(); 1400 + selected_rect_sig.set(Some(sri)); 1401 + } 1391 1402 }, 1392 1403 onmousedown: move |e| { 1393 - if is_sel { 1404 + if is_active && is_sel { 1394 1405 e.stop_propagation(); 1395 1406 edge_drag_active.set(true); 1396 1407 spawn(move_drag_task(sri, [bx, by, bw, bh], sidecar_rects_sig, edge_drag_active, source, active_idx)); ··· 1470 1481 }, 1471 1482 } 1472 1483 } 1473 - } 1474 - } 1475 - } 1476 - } 1477 - } 1478 - if !is_active && seg.kind == SegKind::Card { 1479 - for sr in &card_sidecar_rects_local { 1480 - { 1481 - let img_idx = card_img_names_local.iter().position(|n| n == &sr.src).unwrap_or(0); 1482 - let ib = card_img_boxes_local.get(img_idx).copied().unwrap_or([0.0, 0.0, 1.0, 1.0]); 1483 - let px = (ib[0] + sr.rect[0] as f64 * ib[2]) * iw; 1484 - let py = (ib[1] + sr.rect[1] as f64 * ib[3]) * ih; 1485 - let pw = sr.rect[2] as f64 * ib[2] * iw; 1486 - let ph = sr.rect[3] as f64 * ib[3] * ih; 1487 - let rid = sr.id.clone(); 1488 - rsx! { 1489 - rect { 1490 - key: "{rid}", 1491 - x: "{px}", y: "{py}", 1492 - width: "{pw}", height: "{ph}", 1493 - fill: "rgba(255,220,50,0.35)", 1494 - stroke: "rgb(200,150,0)", 1495 - stroke_width: "1.5", rx: "3", 1496 - style: "pointer-events: none;", 1497 1484 } 1498 1485 } 1499 1486 }
+11 -1
crates/tala/src/main.rs
··· 5 5 use crate::images::Images; 6 6 use crate::review::Review; 7 7 use crate::stats::Stats; 8 - use crate::util::{card_dir, set_card_dir, cards_path}; 8 + use crate::util::{card_dir, set_card_dir, cards_path, cloze_color, set_cloze_color}; 9 9 10 10 mod editor; 11 11 mod images; ··· 119 119 "Choose folder" 120 120 } 121 121 Link { to: Route::Editor {}, "Open editor" } 122 + } 123 + h3 { "Editor" } 124 + p { "Highlight color for cloze blanks in card previews." } 125 + div { style: "display:flex; gap:10px; align-items:center;", 126 + input { 127 + r#type: "color", 128 + value: "{cloze_color()}", 129 + oninput: move |e| set_cloze_color(e.value()), 130 + } 131 + span { style: "font-family:monospace;", "{cloze_color()}" } 122 132 } 123 133 h3 { "Review" } 124 134 p { "Reset all card schedules to due-today. Rect geometry is preserved." }
+64 -2
crates/tala/src/util.rs
··· 2 2 3 3 use dioxus::prelude::*; 4 4 5 + const DEFAULT_CLOZE_COLOR: &str = "#ffdc32"; 6 + 7 + fn config_dir() -> Option<PathBuf> { 8 + let home = std::env::var("HOME").ok()?; 9 + Some(PathBuf::from(home).join(".config").join("tala")) 10 + } 5 11 6 12 fn config_dir_file() -> Option<PathBuf> { 7 - let home = std::env::var("HOME").ok()?; 8 - Some(PathBuf::from(home).join(".config").join("tala").join("dir")) 13 + Some(config_dir()?.join("dir")) 14 + } 15 + 16 + fn config_cloze_color_file() -> Option<PathBuf> { 17 + Some(config_dir()?.join("cloze_color")) 18 + } 19 + 20 + fn load_cloze_color() -> String { 21 + config_cloze_color_file() 22 + .and_then(|p| std::fs::read_to_string(p).ok()) 23 + .map(|s| s.trim().to_string()) 24 + .filter(|s| s.starts_with('#') && s.len() == 7) 25 + .unwrap_or_else(|| DEFAULT_CLOZE_COLOR.to_string()) 26 + } 27 + 28 + fn persist_cloze_color(hex: &str) { 29 + if let Some(f) = config_cloze_color_file() { 30 + if let Some(parent) = f.parent() { 31 + let _ = std::fs::create_dir_all(parent); 32 + } 33 + let _ = std::fs::write(&f, hex); 34 + } 35 + } 36 + 37 + /// Parse `#rrggbb` into (r, g, b). Falls back to default on error. 38 + pub fn parse_hex_color(hex: &str) -> (u8, u8, u8) { 39 + let h = hex.trim_start_matches('#'); 40 + if h.len() != 6 { 41 + return (255, 220, 50); 42 + } 43 + let r = u8::from_str_radix(&h[0..2], 16).unwrap_or(255); 44 + let g = u8::from_str_radix(&h[2..4], 16).unwrap_or(220); 45 + let b = u8::from_str_radix(&h[4..6], 16).unwrap_or(50); 46 + (r, g, b) 47 + } 48 + 49 + /// CSS fill string for cloze highlight (35% alpha). 50 + pub fn cloze_fill_css(hex: &str) -> String { 51 + let (r, g, b) = parse_hex_color(hex); 52 + format!("rgba({r},{g},{b},0.35)") 53 + } 54 + 55 + /// CSS stroke string for cloze highlight (full opacity). 56 + pub fn cloze_stroke_css(hex: &str) -> String { 57 + let (r, g, b) = parse_hex_color(hex); 58 + format!("rgb({r},{g},{b})") 59 + } 60 + 61 + static CLOZE_COLOR: GlobalSignal<String> = Signal::global(load_cloze_color); 62 + 63 + pub fn cloze_color() -> String { 64 + CLOZE_COLOR.read().clone() 65 + } 66 + 67 + pub fn set_cloze_color(hex: String) { 68 + persist_cloze_color(&hex); 69 + *CLOZE_COLOR.write() = hex; 9 70 } 10 71 11 72 fn load_saved_dir() -> Option<PathBuf> { ··· 27 88 let _ = std::fs::write(&f, path.display().to_string()); 28 89 } 29 90 } 91 + 30 92 31 93 32 94 #[derive(Clone, PartialEq)]