use std::borrow::Cow;
use freya_components::scrollviews::{
ScrollController,
ScrollEvent,
VirtualScrollView,
};
use freya_core::prelude::*;
use freya_edit::EditableEvent;
use crate::{
editor_data::CodeEditorData,
editor_line::EditorLineUI,
editor_theme::{
DEFAULT_EDITOR_THEME,
EditorTheme,
},
};
#[derive(PartialEq, Clone)]
pub struct CodeEditor {
editor: Writable<CodeEditorData>,
font_size: f32,
line_height: f32,
read_only: bool,
gutter: bool,
show_whitespace: bool,
font_family: Cow<'static, str>,
a11y_id: AccessibilityId,
a11y_auto_focus: bool,
theme: Readable<EditorTheme>,
on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
}
impl CodeEditor {
pub fn new(editor: impl Into<Writable<CodeEditorData>>, a11y_id: AccessibilityId) -> Self {
Self {
editor: editor.into(),
font_size: 14.0,
line_height: 1.4,
read_only: false,
gutter: true,
show_whitespace: true,
font_family: Cow::Borrowed("Jetbrains Mono"),
a11y_id,
a11y_auto_focus: false,
theme: DEFAULT_EDITOR_THEME.into(),
on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| {
e.stop_propagation();
if let Key::Named(NamedKey::Tab) = &e.key {
e.prevent_default();
}
true
}),
}
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn line_height(mut self, height: f32) -> Self {
self.line_height = height;
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn gutter(mut self, gutter: bool) -> Self {
self.gutter = gutter;
self
}
pub fn show_whitespace(mut self, show_whitespace: bool) -> Self {
self.show_whitespace = show_whitespace;
self
}
pub fn font_family(mut self, font_family: impl Into<Cow<'static, str>>) -> Self {
self.font_family = font_family.into();
self
}
pub fn a11y_auto_focus(mut self, a11y_auto_focus: bool) -> Self {
self.a11y_auto_focus = a11y_auto_focus;
self
}
pub fn theme(mut self, theme: impl IntoReadable<EditorTheme>) -> Self {
self.theme = theme.into_readable();
self
}
pub fn on_pre_key_down(
mut self,
on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
) -> Self {
self.on_pre_key_down = on_pre_key_down.into();
self
}
}
impl Component for CodeEditor {
fn render(&self) -> impl IntoElement {
let CodeEditor {
editor,
font_size,
line_height,
read_only,
gutter,
show_whitespace,
font_family,
a11y_id,
a11y_auto_focus,
theme,
on_pre_key_down,
} = self.clone();
let editor_data = editor.read();
let scroll_controller = use_hook(|| {
let notifier = State::create(());
let requests = State::create(vec![]);
ScrollController::managed(
notifier,
requests,
State::create(Callback::new({
let mut editor = editor.clone();
move |ev| {
editor.write_if(|mut editor| {
let current = editor.scrolls;
match ev {
ScrollEvent::X(x) => {
editor.scrolls.0 = x;
}
ScrollEvent::Y(y) => {
editor.scrolls.1 = y;
}
}
current != editor.scrolls
})
}
})),
State::create(Callback::new({
let editor = editor.clone();
move |_| {
let editor = editor.read();
editor.scrolls
}
})),
)
});
let line_height = (font_size * line_height).floor();
let lines_len = editor_data.metrics.syntax_blocks.len();
let on_key_up = {
let mut editor = editor.clone();
let font_family = font_family.clone();
move |e: Event<KeyboardEventData>| {
editor.write_if(|mut editor| {
editor.process(
font_size,
&font_family,
EditableEvent::KeyUp { key: &e.key },
)
});
}
};
let on_key_down = {
let mut editor = editor.clone();
let font_family = font_family.clone();
move |e: Event<KeyboardEventData>| {
const LINES_JUMP_ALT: usize = 5;
const LINES_JUMP_CONTROL: usize = 3;
let key = e.key.clone();
let modifiers = e.modifiers;
if !on_pre_key_down.call(e) {
return;
}
editor.write_if(|mut editor| {
let lines_jump = (line_height * LINES_JUMP_ALT as f32).ceil() as i32;
let min_height = -(lines_len as f32 * line_height) as i32;
let max_height = 0; let current_scroll = editor.scrolls.1;
let events = match &key {
Key::Named(NamedKey::ArrowUp) if modifiers.contains(Modifiers::ALT) => {
let jump = (current_scroll + lines_jump).clamp(min_height, max_height);
editor.scrolls.1 = jump;
(0..LINES_JUMP_ALT)
.map(|_| EditableEvent::KeyDown {
key: &key,
modifiers,
})
.collect::<Vec<EditableEvent>>()
}
Key::Named(NamedKey::ArrowDown) if modifiers.contains(Modifiers::ALT) => {
let jump = (current_scroll - lines_jump).clamp(min_height, max_height);
editor.scrolls.1 = jump;
(0..LINES_JUMP_ALT)
.map(|_| EditableEvent::KeyDown {
key: &key,
modifiers,
})
.collect::<Vec<EditableEvent>>()
}
Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::ArrowUp)
if modifiers.contains(Modifiers::CONTROL) =>
{
(0..LINES_JUMP_CONTROL)
.map(|_| EditableEvent::KeyDown {
key: &key,
modifiers,
})
.collect::<Vec<EditableEvent>>()
}
_ => vec![EditableEvent::KeyDown {
key: &key,
modifiers,
}],
};
let mut changed = false;
for event in events {
changed |= editor.process(font_size, &font_family, event);
}
changed
});
}
};
let on_global_pointer_press = {
let mut editor = editor.clone();
let font_family = font_family.clone();
move |_: Event<PointerEventData>| {
editor.write_if(|mut editor_editor| {
editor_editor.process(font_size, &font_family, EditableEvent::Release)
});
}
};
rect()
.a11y_auto_focus(a11y_auto_focus)
.a11y_focusable(true)
.a11y_id(a11y_id)
.a11y_role(AccessibilityRole::TextInput)
.expanded()
.background(theme.read().background)
.maybe(!read_only, |el| {
el.on_key_down(on_key_down).on_key_up(on_key_up)
})
.on_global_pointer_press(on_global_pointer_press)
.child(
VirtualScrollView::new(move |line_index, _| {
EditorLineUI {
editor: editor.clone(),
font_size,
line_height,
line_index,
read_only,
gutter,
show_whitespace,
font_family: font_family.clone(),
theme: theme.clone(),
a11y_id,
}
.into()
})
.scroll_controller(scroll_controller)
.length(lines_len)
.item_size(line_height),
)
}
}