Skip to main content

logicaffeine_web/ui/components/
formula_editor.rs

1//! LaTeX formula editor with live preview.
2//!
3//! Provides a split-pane interface with a LaTeX input area and real-time
4//! KaTeX rendering. Includes quick-insert buttons for common symbols.
5//!
6//! # Components
7//!
8//! - [`FormulaEditor`] - Full editor with input, quick-insert, and preview
9//! - [`LatexPreview`] - Standalone preview component
10//!
11//! # Props (FormulaEditor)
12//!
13//! - `latex` - Current LaTeX source
14//! - `on_change` - Callback when source changes
15//! - `on_insert` - Optional callback for quick-insert buttons
16
17use dioxus::prelude::*;
18use super::katex::KatexSpan;
19
20const FORMULA_EDITOR_STYLE: &str = r#"
21.formula-editor {
22    display: flex;
23    flex-direction: column;
24    height: 100%;
25    background: #0f1419;
26}
27
28.formula-editor-input {
29    flex: 1;
30    display: flex;
31    flex-direction: column;
32    padding: 16px;
33    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
34}
35
36.formula-editor-label {
37    font-size: 12px;
38    font-weight: 600;
39    color: rgba(255, 255, 255, 0.5);
40    margin-bottom: 8px;
41    text-transform: uppercase;
42    letter-spacing: 0.5px;
43}
44
45.formula-textarea {
46    flex: 1;
47    width: 100%;
48    min-height: 120px;
49    background: rgba(255, 255, 255, 0.04);
50    border: 1px solid rgba(255, 255, 255, 0.1);
51    border-radius: 8px;
52    padding: 14px;
53    font-size: 15px;
54    font-family: 'SF Mono', 'Fira Code', monospace;
55    color: rgba(255, 255, 255, 0.9);
56    resize: none;
57    outline: none;
58    line-height: 1.6;
59}
60
61.formula-textarea:focus {
62    border-color: rgba(102, 126, 234, 0.5);
63    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
64}
65
66.formula-textarea::placeholder {
67    color: rgba(255, 255, 255, 0.3);
68}
69
70.formula-preview {
71    padding: 24px;
72    display: flex;
73    align-items: center;
74    justify-content: center;
75    min-height: 150px;
76    background: rgba(255, 255, 255, 0.02);
77}
78
79.formula-preview-empty {
80    color: rgba(255, 255, 255, 0.3);
81    font-size: 14px;
82    text-align: center;
83}
84
85.formula-preview .katex-display {
86    font-size: 1.8em;
87}
88
89.formula-error {
90    color: #e06c75;
91    padding: 12px 16px;
92    background: rgba(224, 108, 117, 0.1);
93    border-radius: 6px;
94    font-size: 13px;
95    margin: 16px;
96}
97
98/* Quick insert bar */
99.formula-quick-insert {
100    display: flex;
101    gap: 4px;
102    padding: 8px 16px;
103    border-top: 1px solid rgba(255, 255, 255, 0.06);
104    background: rgba(255, 255, 255, 0.02);
105    flex-wrap: wrap;
106}
107
108.quick-insert-btn {
109    padding: 6px 10px;
110    background: rgba(255, 255, 255, 0.04);
111    border: 1px solid rgba(255, 255, 255, 0.08);
112    border-radius: 4px;
113    color: rgba(255, 255, 255, 0.7);
114    font-size: 14px;
115    cursor: pointer;
116    transition: all 0.1s ease;
117}
118
119.quick-insert-btn:hover {
120    background: rgba(102, 126, 234, 0.15);
121    border-color: rgba(102, 126, 234, 0.3);
122    color: #667eea;
123}
124
125/* Mobile */
126@media (max-width: 768px) {
127    .formula-textarea {
128        font-size: 16px; /* Prevent iOS zoom */
129        min-height: 100px;
130    }
131
132    .formula-preview .katex-display {
133        font-size: 1.5em;
134    }
135}
136"#;
137
138/// Common LaTeX snippets for quick insertion.
139const QUICK_INSERTS: &[(&str, &str)] = &[
140    ("\u{2200}", "\\forall "),
141    ("\u{2203}", "\\exists "),
142    ("\u{2192}", "\\to "),
143    ("\u{2227}", "\\land "),
144    ("\u{2228}", "\\lor "),
145    ("\u{00AC}", "\\neg "),
146    ("frac", "\\frac{}{}"),
147    ("sum", "\\sum_{i=0}^{n}"),
148    ("sqrt", "\\sqrt{}"),
149];
150
151#[component]
152pub fn FormulaEditor(
153    latex: String,
154    on_change: EventHandler<String>,
155    on_insert: Option<EventHandler<String>>,
156) -> Element {
157    // Track if we have a render error
158    let has_content = !latex.trim().is_empty();
159
160    rsx! {
161        style { "{FORMULA_EDITOR_STYLE}" }
162
163        div { class: "formula-editor",
164            // Input area
165            div { class: "formula-editor-input",
166                label { class: "formula-editor-label", "LaTeX Input" }
167                textarea {
168                    class: "formula-textarea",
169                    placeholder: "Enter LaTeX formula... (e.g., \\forall x \\in A: P(x))",
170                    value: "{latex}",
171                    oninput: move |e| on_change.call(e.value()),
172                    spellcheck: "false",
173                }
174            }
175
176            // Quick insert bar
177            div { class: "formula-quick-insert",
178                for (label, insert) in QUICK_INSERTS.iter() {
179                    button {
180                        class: "quick-insert-btn",
181                        key: "{insert}",
182                        onclick: {
183                            let insert = insert.to_string();
184                            let handler = on_insert.clone();
185                            move |_| {
186                                if let Some(ref h) = handler {
187                                    h.call(insert.clone());
188                                }
189                            }
190                        },
191                        "{label}"
192                    }
193                }
194            }
195
196            // Live preview using existing KatexSpan
197            div { class: "formula-preview",
198                if has_content {
199                    KatexSpan { latex: latex.clone(), display: true }
200                } else {
201                    div { class: "formula-preview-empty",
202                        "Preview will appear here"
203                    }
204                }
205            }
206        }
207    }
208}
209
210/// Standalone preview component that just renders LaTeX.
211#[component]
212pub fn LatexPreview(latex: String) -> Element {
213    let has_content = !latex.trim().is_empty();
214
215    rsx! {
216        div { class: "formula-preview",
217            style: "height: 100%; background: #12161c;",
218            if has_content {
219                KatexSpan { latex: latex, display: true }
220            } else {
221                div { class: "formula-preview-empty",
222                    "Enter a formula to see the preview"
223                }
224            }
225        }
226    }
227}