Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 4 additions & 50 deletions crates/app/src/controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

use codeframe_models::{Background, FontChoice, Language, ThemeChoice};
use leptos::prelude::*;
use wasm_bindgen::JsCast;

use crate::state::{Settings, SAMPLE_CODE};
use crate::editor::CodeEditor;
use crate::state::Settings;

const SCALE_PRESETS: [f64; 4] = [1.0, 2.0, 4.0, 8.0];

Expand Down Expand Up @@ -259,64 +259,18 @@ pub fn Controls(settings: Settings) -> impl IntoView {
<div class="section-body">
<div>
<label class="control-label" for="code-input">{move || if settings.split_enabled.get() { "Left panel code" } else { "Code" }}</label>
<textarea
id="code-input"
class="code-input"
rows="12"
spellcheck="false"
autocomplete="off"
on:input=move |ev| settings.code.set(event_target_value(&ev))
on:keydown=move |ev| {
if ev.key() == "Tab" {
ev.prevent_default();
let target = ev.target().unwrap();
let textarea: web_sys::HtmlTextAreaElement = target.unchecked_into();
let start = textarea.selection_start().unwrap_or_default().unwrap_or(0) as usize;
let end = textarea.selection_end().unwrap_or_default().unwrap_or(0) as usize;
let value = textarea.value();
let new_value = format!("{} {}", &value[..start], &value[end..]);
settings.code.set(new_value.clone());
textarea.set_value(&new_value);
let pos = (start + 4) as u32;
let _ = textarea.set_selection_range(pos, pos);
}
}
>{SAMPLE_CODE}</textarea>
<CodeEditor id="code-input" code=settings.code language=settings.language theme=settings.theme />
</div>

<Toggle id="split-toggle" checked=settings.split_enabled label="Split-screen comparison" />

{move || {
settings.split_enabled.get().then(|| {
let code_signal = settings.split_code;
view! {
<div class="split-controls">
<div>
<label class="control-label" for="split-code-input">"Right panel code"</label>
<textarea
id="split-code-input"
class="code-input"
rows="12"
spellcheck="false"
autocomplete="off"
prop:value=move || code_signal.get()
on:input=move |ev| code_signal.set(event_target_value(&ev))
on:keydown=move |ev| {
if ev.key() == "Tab" {
ev.prevent_default();
let target = ev.target().unwrap();
let textarea: web_sys::HtmlTextAreaElement = target.unchecked_into();
let start = textarea.selection_start().unwrap_or_default().unwrap_or(0) as usize;
let end = textarea.selection_end().unwrap_or_default().unwrap_or(0) as usize;
let value = textarea.value();
let new_value = format!("{} {}", &value[..start], &value[end..]);
code_signal.set(new_value.clone());
textarea.set_value(&new_value);
let pos = (start + 4) as u32;
let _ = textarea.set_selection_range(pos, pos);
}
}
></textarea>
<CodeEditor id="split-code-input" code=settings.split_code language=settings.split_language theme=settings.split_theme />
</div>
<div class="control-row">
<div>
Expand Down
94 changes: 94 additions & 0 deletions crates/app/src/editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Syntax-highlighted code input: a transparent-text textarea overlaid on a
//! `<pre>` rendered from the token stream, so the input mirrors the export.

use codeframe_models::{Language, ThemeChoice};
use leptos::prelude::*;
use wasm_bindgen::JsCast;

/// Palette-derived editor background + caret color, painted on the wrapper so
/// the input matches the export canvas for the same theme.
fn editor_colors(theme: ThemeChoice) -> String {
match codeframe_highlighter::theme_palette(theme) {
Ok(palette) => format!(
"background: {}; caret-color: {};",
palette.background.to_css(),
palette.foreground.to_css()
),
Err(_) => "caret-color: var(--ink);".to_string(),
}
}

/// A code textarea with live syntax highlighting behind the text.
///
/// The `<pre>` layer carries the token colors (HTML from
/// `codeframe_highlighter::highlight_to_html`); the textarea above it uses
/// transparent text so only the caret and selection are visible. Both layers
/// share identical font metrics and wrapping, and the `<pre>` scrolls in
/// lockstep with the textarea.
#[component]
pub fn CodeEditor(
id: &'static str,
code: RwSignal<String>,
language: RwSignal<Language>,
theme: RwSignal<ThemeChoice>,
) -> impl IntoView {
let pre_ref: NodeRef<leptos::html::Pre> = NodeRef::new();
let textarea_ref: NodeRef<leptos::html::Textarea> = NodeRef::new();

let html = Memo::new(move |_| {
let code = code.get();
let language = language.get();
let theme = theme.get();
match codeframe_highlighter::highlight_to_html(&code, language, theme) {
Ok(html) => html,
Err(e) => format!(
"<span style=\"color:var(--warning)\">{}</span>",
codeframe_highlighter::escape_html(&e.to_string())
),
}
});

view! {
<div class="code-editor" style=move || editor_colors(theme.get())>
<pre
class="code-editor-highlight"
aria-hidden="true"
node_ref=pre_ref
inner_html=move || html.get()
></pre>
<textarea
id=id
class="code-input"
rows="12"
spellcheck="false"
autocomplete="off"
node_ref=textarea_ref
// Set once at creation, then let the DOM own the value: a
// reactive binding would rewrite the textarea on every input
// and stomp the native caret during paste/undo (cursor jumps).
prop:value=code.get_untracked()
on:input=move |ev| code.set(event_target_value(&ev))
on:keydown=move |ev| {
if ev.key() == "Tab" {
ev.prevent_default();
let target = ev.target().unwrap();
let textarea: web_sys::HtmlTextAreaElement = target.unchecked_into();
let start = textarea.selection_start().unwrap_or_default().unwrap_or(0);
// Native insertion - no manual byte slicing, so
// multi-byte characters before the caret are safe.
let _ = textarea.set_range_text(" ");
code.set(textarea.value());
let pos = start + 4;
let _ = textarea.set_selection_range(pos, pos);
}
}
on:scroll=move |_| {
if let (Some(pre), Some(textarea)) = (pre_ref.get(), textarea_ref.get()) {
pre.set_scroll_top(textarea.scroll_top());
pre.set_scroll_left(textarea.scroll_left());
}
}
></textarea>
</div>
}
}
1 change: 1 addition & 0 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#![deny(unsafe_code)]

mod controls;
mod editor;
mod export;
mod fonts;
mod preview;
Expand Down
Loading
Loading