fix another round of fixes of M3
This commit is contained in:
@@ -1,8 +1,17 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use syntect::highlighting::{Style, ThemeSet};
|
use syntect::highlighting::{Style, ThemeSet};
|
||||||
use syntect::parsing::{SyntaxReference, SyntaxSet};
|
use syntect::parsing::{SyntaxReference, SyntaxSet};
|
||||||
|
|
||||||
|
/// Global highlighter singleton (expensive to create, safe to share).
|
||||||
|
static GLOBAL_HIGHLIGHTER: LazyLock<Highlighter> = LazyLock::new(Highlighter::new);
|
||||||
|
|
||||||
|
/// Return a reference to the global syntax highlighter.
|
||||||
|
pub fn highlighter() -> &'static Highlighter {
|
||||||
|
&GLOBAL_HIGHLIGHTER
|
||||||
|
}
|
||||||
|
|
||||||
/// Syntax highlighter using syntect with line-level caching.
|
/// Syntax highlighter using syntect with line-level caching.
|
||||||
pub struct Highlighter {
|
pub struct Highlighter {
|
||||||
syntax_set: SyntaxSet,
|
syntax_set: SyntaxSet,
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ pub mod history;
|
|||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
pub use buffer::{EditorBuffer, Selection};
|
pub use buffer::{EditorBuffer, Selection};
|
||||||
pub use highlight::Highlighter;
|
pub use highlight::{highlighter, Highlighter};
|
||||||
pub use widget::{CodeEditor, EditorMessage, mono_metrics};
|
pub use widget::{CodeEditor, EditorMessage, mono_metrics};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use cosmic_text::{Attrs, Buffer as CosmicBuffer, Family, FontSystem, Metrics, Shaping};
|
use cosmic_text::{Attrs, Buffer as CosmicBuffer, Family, FontSystem, Metrics, Shaping};
|
||||||
@@ -96,7 +97,7 @@ fn syntect_to_iced(c: syntect::highlighting::Color) -> Color {
|
|||||||
|
|
||||||
/// A syntax-highlighting code editor widget for Iced.
|
/// A syntax-highlighting code editor widget for Iced.
|
||||||
pub struct CodeEditor<'a, Message> {
|
pub struct CodeEditor<'a, Message> {
|
||||||
buffer: &'a mut EditorBuffer,
|
buffer: &'a RefCell<EditorBuffer>,
|
||||||
highlighter: &'a Highlighter,
|
highlighter: &'a Highlighter,
|
||||||
extension: &'a str,
|
extension: &'a str,
|
||||||
on_change: Option<Box<dyn Fn(EditorMessage) -> Message + 'a>>,
|
on_change: Option<Box<dyn Fn(EditorMessage) -> Message + 'a>>,
|
||||||
@@ -104,7 +105,7 @@ pub struct CodeEditor<'a, Message> {
|
|||||||
|
|
||||||
impl<'a, Message> CodeEditor<'a, Message> {
|
impl<'a, Message> CodeEditor<'a, Message> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
buffer: &'a mut EditorBuffer,
|
buffer: &'a RefCell<EditorBuffer>,
|
||||||
highlighter: &'a Highlighter,
|
highlighter: &'a Highlighter,
|
||||||
extension: &'a str,
|
extension: &'a str,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -161,6 +162,7 @@ where
|
|||||||
) {
|
) {
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
let state = tree.state.downcast_ref::<EditorState>();
|
let state = tree.state.downcast_ref::<EditorState>();
|
||||||
|
let buf = self.buffer.borrow();
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
renderer.fill_quad(
|
renderer.fill_quad(
|
||||||
@@ -187,14 +189,14 @@ where
|
|||||||
);
|
);
|
||||||
|
|
||||||
let metrics = mono_metrics();
|
let metrics = mono_metrics();
|
||||||
let (cursor_line, cursor_col) = self.buffer.cursor();
|
let (cursor_line, cursor_col) = buf.cursor();
|
||||||
let scroll = self.buffer.scroll_offset();
|
let scroll = buf.scroll_offset();
|
||||||
let visible_lines = (bounds.height / metrics.line_height) as usize + 1;
|
let visible_lines = (bounds.height / metrics.line_height) as usize + 1;
|
||||||
let selection = self.buffer.selection();
|
let selection = buf.selection();
|
||||||
|
|
||||||
// Pre-compute highlighted lines for visible range
|
// Pre-compute highlighted lines for visible range
|
||||||
let syntax = self.highlighter.syntax_for_extension(self.extension);
|
let syntax = self.highlighter.syntax_for_extension(self.extension);
|
||||||
let full_text = self.buffer.text();
|
let full_text = buf.text();
|
||||||
let highlighted = self.highlighter.highlight_lines(&full_text, syntax);
|
let highlighted = self.highlighter.highlight_lines(&full_text, syntax);
|
||||||
|
|
||||||
let font = iced::Font::MONOSPACE;
|
let font = iced::Font::MONOSPACE;
|
||||||
@@ -202,7 +204,7 @@ where
|
|||||||
// Render visible lines
|
// Render visible lines
|
||||||
for vis_idx in 0..visible_lines {
|
for vis_idx in 0..visible_lines {
|
||||||
let line_idx = scroll + vis_idx;
|
let line_idx = scroll + vis_idx;
|
||||||
if line_idx >= self.buffer.line_count() {
|
if line_idx >= buf.line_count() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +219,7 @@ where
|
|||||||
if let Some(sel) = selection {
|
if let Some(sel) = selection {
|
||||||
if !sel.is_empty() {
|
if !sel.is_empty() {
|
||||||
let (start, end) = sel.ordered();
|
let (start, end) = sel.ordered();
|
||||||
let line_len = self
|
let line_len = buf
|
||||||
.buffer
|
|
||||||
.line(line_idx)
|
.line(line_idx)
|
||||||
.map(|l| {
|
.map(|l| {
|
||||||
let len = l.len_chars();
|
let len = l.len_chars();
|
||||||
@@ -319,7 +320,7 @@ where
|
|||||||
);
|
);
|
||||||
x_off += span_width;
|
x_off += span_width;
|
||||||
}
|
}
|
||||||
} else if let Some(line) = self.buffer.line(line_idx) {
|
} else if let Some(line) = buf.line(line_idx) {
|
||||||
// Fallback: plain text rendering
|
// Fallback: plain text rendering
|
||||||
let mut line_text: String = line.chars().collect();
|
let mut line_text: String = line.chars().collect();
|
||||||
if line_text.ends_with('\n') {
|
if line_text.ends_with('\n') {
|
||||||
@@ -390,8 +391,9 @@ where
|
|||||||
state.is_dragging = true;
|
state.is_dragging = true;
|
||||||
|
|
||||||
if let Some(pos) = cursor.position_in(bounds) {
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
let line = (pos.y / metrics.line_height) as usize
|
let line = (pos.y / metrics.line_height) as usize
|
||||||
+ self.buffer.scroll_offset();
|
+ buf.scroll_offset();
|
||||||
let col = ((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width)
|
let col = ((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width)
|
||||||
as usize;
|
as usize;
|
||||||
|
|
||||||
@@ -405,11 +407,11 @@ where
|
|||||||
&& (state.last_click_col as isize - col as isize).unsigned_abs() < 3;
|
&& (state.last_click_col as isize - col as isize).unsigned_abs() < 3;
|
||||||
|
|
||||||
if is_double_click {
|
if is_double_click {
|
||||||
self.buffer.select_word_at(line, col);
|
buf.select_word_at(line, col);
|
||||||
state.last_click_time = None; // reset
|
state.last_click_time = None; // reset
|
||||||
} else {
|
} else {
|
||||||
self.buffer.clear_selection();
|
buf.clear_selection();
|
||||||
self.buffer.set_cursor(line, col);
|
buf.set_cursor(line, col);
|
||||||
state.last_click_time = Some(now);
|
state.last_click_time = Some(now);
|
||||||
state.last_click_line = line;
|
state.last_click_line = line;
|
||||||
state.last_click_col = col;
|
state.last_click_col = col;
|
||||||
@@ -426,15 +428,16 @@ where
|
|||||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||||
if state.is_dragging && state.is_focused {
|
if state.is_dragging && state.is_focused {
|
||||||
if let Some(pos) = cursor.position_in(bounds) {
|
if let Some(pos) = cursor.position_in(bounds) {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
let line = (pos.y / metrics.line_height) as usize
|
let line = (pos.y / metrics.line_height) as usize
|
||||||
+ self.buffer.scroll_offset();
|
+ buf.scroll_offset();
|
||||||
let col = ((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width)
|
let col = ((pos.x - GUTTER_WIDTH - 8.0).max(0.0) / metrics.char_width)
|
||||||
as usize;
|
as usize;
|
||||||
// Extend selection by simulating shift+movement
|
// Extend selection by simulating shift+movement
|
||||||
let clamped_line = line.min(self.buffer.line_count().saturating_sub(1));
|
let clamped_line = line.min(buf.line_count().saturating_sub(1));
|
||||||
let clamped_col = if clamped_line < self.buffer.line_count() {
|
let clamped_col = if clamped_line < buf.line_count() {
|
||||||
col.min(
|
col.min(
|
||||||
self.buffer
|
buf
|
||||||
.line(clamped_line)
|
.line(clamped_line)
|
||||||
.map(|l| {
|
.map(|l| {
|
||||||
let len = l.len_chars();
|
let len = l.len_chars();
|
||||||
@@ -449,20 +452,21 @@ where
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
self.buffer
|
let anchor_line = buf
|
||||||
.set_selection(
|
|
||||||
self.buffer
|
|
||||||
.selection()
|
.selection()
|
||||||
.map(|s| s.anchor_line)
|
.map(|s| s.anchor_line)
|
||||||
.unwrap_or(self.buffer.cursor().0),
|
.unwrap_or(buf.cursor().0);
|
||||||
self.buffer
|
let anchor_col = buf
|
||||||
.selection()
|
.selection()
|
||||||
.map(|s| s.anchor_col)
|
.map(|s| s.anchor_col)
|
||||||
.unwrap_or(self.buffer.cursor().1),
|
.unwrap_or(buf.cursor().1);
|
||||||
|
buf.set_selection(
|
||||||
|
anchor_line,
|
||||||
|
anchor_col,
|
||||||
clamped_line,
|
clamped_line,
|
||||||
clamped_col,
|
clamped_col,
|
||||||
);
|
);
|
||||||
self.buffer.set_cursor(clamped_line, clamped_col);
|
buf.set_cursor(clamped_line, clamped_col);
|
||||||
}
|
}
|
||||||
return Status::Captured;
|
return Status::Captured;
|
||||||
}
|
}
|
||||||
@@ -474,7 +478,7 @@ where
|
|||||||
-(y / metrics.line_height) as isize
|
-(y / metrics.line_height) as isize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.buffer.scroll_by(lines);
|
self.buffer.borrow_mut().scroll_by(lines);
|
||||||
return Status::Captured;
|
return Status::Captured;
|
||||||
}
|
}
|
||||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
@@ -488,35 +492,34 @@ where
|
|||||||
match key {
|
match key {
|
||||||
// Cmd+Z = undo, Cmd+Shift+Z = redo
|
// Cmd+Z = undo, Cmd+Shift+Z = redo
|
||||||
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "z" => {
|
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "z" => {
|
||||||
if is_shift {
|
{
|
||||||
self.buffer.redo();
|
let mut buf = self.buffer.borrow_mut();
|
||||||
} else {
|
if is_shift { buf.redo(); } else { buf.undo(); }
|
||||||
self.buffer.undo();
|
|
||||||
}
|
}
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
// Cmd+Y = redo (Windows convention)
|
// Cmd+Y = redo (Windows convention)
|
||||||
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "y" => {
|
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "y" => {
|
||||||
self.buffer.redo();
|
self.buffer.borrow_mut().redo();
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
// Cmd+A = select all
|
// Cmd+A = select all
|
||||||
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "a" => {
|
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "a" => {
|
||||||
self.buffer.select_all();
|
self.buffer.borrow_mut().select_all();
|
||||||
}
|
}
|
||||||
// Cmd+C = copy
|
// Cmd+C = copy
|
||||||
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "c" => {
|
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "c" => {
|
||||||
let text = self.buffer.selected_text();
|
let text = self.buffer.borrow().selected_text();
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
clipboard.write(iced::advanced::clipboard::Kind::Standard, text);
|
clipboard.write(iced::advanced::clipboard::Kind::Standard, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cmd+X = cut
|
// Cmd+X = cut
|
||||||
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "x" => {
|
keyboard::Key::Character(ref c) if is_cmd && c.as_str() == "x" => {
|
||||||
let text = self.buffer.selected_text();
|
let text = self.buffer.borrow().selected_text();
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
clipboard.write(iced::advanced::clipboard::Kind::Standard, text);
|
clipboard.write(iced::advanced::clipboard::Kind::Standard, text);
|
||||||
self.buffer.delete_selection();
|
self.buffer.borrow_mut().delete_selection();
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,7 +528,7 @@ where
|
|||||||
if let Some(text) =
|
if let Some(text) =
|
||||||
clipboard.read(iced::advanced::clipboard::Kind::Standard)
|
clipboard.read(iced::advanced::clipboard::Kind::Standard)
|
||||||
{
|
{
|
||||||
self.buffer.insert(&text);
|
self.buffer.borrow_mut().insert(&text);
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,99 +540,110 @@ where
|
|||||||
}
|
}
|
||||||
// Arrow keys with modifiers
|
// Arrow keys with modifiers
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift {
|
if is_shift {
|
||||||
self.buffer.select_up();
|
buf.select_up();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_up();
|
buf.move_up();
|
||||||
}
|
}
|
||||||
self.buffer.ensure_cursor_visible(vis);
|
buf.ensure_cursor_visible(vis);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift {
|
if is_shift {
|
||||||
self.buffer.select_down();
|
buf.select_down();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_down();
|
buf.move_down();
|
||||||
}
|
}
|
||||||
self.buffer.ensure_cursor_visible(vis);
|
buf.ensure_cursor_visible(vis);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift && is_alt {
|
if is_shift && is_alt {
|
||||||
self.buffer.select_word_left();
|
buf.select_word_left();
|
||||||
} else if is_shift {
|
} else if is_shift {
|
||||||
self.buffer.select_left();
|
buf.select_left();
|
||||||
} else if is_alt {
|
} else if is_alt {
|
||||||
self.buffer.move_word_left();
|
buf.move_word_left();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_left();
|
buf.move_left();
|
||||||
}
|
}
|
||||||
self.buffer.ensure_cursor_visible(vis);
|
buf.ensure_cursor_visible(vis);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift && is_alt {
|
if is_shift && is_alt {
|
||||||
self.buffer.select_word_right();
|
buf.select_word_right();
|
||||||
} else if is_shift {
|
} else if is_shift {
|
||||||
self.buffer.select_right();
|
buf.select_right();
|
||||||
} else if is_alt {
|
} else if is_alt {
|
||||||
self.buffer.move_word_right();
|
buf.move_word_right();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_right();
|
buf.move_right();
|
||||||
}
|
}
|
||||||
self.buffer.ensure_cursor_visible(vis);
|
buf.ensure_cursor_visible(vis);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Home) => {
|
keyboard::Key::Named(keyboard::key::Named::Home) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift {
|
if is_shift {
|
||||||
self.buffer.select_home();
|
buf.select_home();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_home();
|
buf.move_home();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::End) => {
|
keyboard::Key::Named(keyboard::key::Named::End) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift {
|
if is_shift {
|
||||||
self.buffer.select_end();
|
buf.select_end();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_end();
|
buf.move_end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::PageUp) => {
|
keyboard::Key::Named(keyboard::key::Named::PageUp) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift {
|
if is_shift {
|
||||||
self.buffer.select_page_up(vis);
|
buf.select_page_up(vis);
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_page_up(vis);
|
buf.move_page_up(vis);
|
||||||
}
|
}
|
||||||
self.buffer.ensure_cursor_visible(vis);
|
buf.ensure_cursor_visible(vis);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::PageDown) => {
|
keyboard::Key::Named(keyboard::key::Named::PageDown) => {
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_shift {
|
if is_shift {
|
||||||
self.buffer.select_page_down(vis);
|
buf.select_page_down(vis);
|
||||||
} else {
|
} else {
|
||||||
self.buffer.move_page_down(vis);
|
buf.move_page_down(vis);
|
||||||
}
|
}
|
||||||
self.buffer.ensure_cursor_visible(vis);
|
buf.ensure_cursor_visible(vis);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Backspace) => {
|
keyboard::Key::Named(keyboard::key::Named::Backspace) => {
|
||||||
|
{
|
||||||
|
let mut buf = self.buffer.borrow_mut();
|
||||||
if is_alt {
|
if is_alt {
|
||||||
// Option+Backspace = delete word left
|
// Option+Backspace = delete word left
|
||||||
self.buffer.select_word_left();
|
buf.select_word_left();
|
||||||
self.buffer.delete_selection();
|
buf.delete_selection();
|
||||||
} else {
|
} else {
|
||||||
self.buffer.backspace();
|
buf.backspace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Delete) => {
|
keyboard::Key::Named(keyboard::key::Named::Delete) => {
|
||||||
self.buffer.delete_forward();
|
self.buffer.borrow_mut().delete_forward();
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
||||||
self.buffer.insert("\n");
|
self.buffer.borrow_mut().insert("\n");
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(keyboard::key::Named::Tab) => {
|
keyboard::Key::Named(keyboard::key::Named::Tab) => {
|
||||||
self.buffer.insert(" ");
|
self.buffer.borrow_mut().insert(" ");
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
keyboard::Key::Character(ref c) if !is_cmd => {
|
keyboard::Key::Character(ref c) if !is_cmd => {
|
||||||
self.buffer.insert(c);
|
self.buffer.borrow_mut().insert(c);
|
||||||
self.emit_change(shell);
|
self.emit_change(shell);
|
||||||
}
|
}
|
||||||
_ => return Status::Ignored,
|
_ => return Status::Ignored,
|
||||||
@@ -645,7 +659,7 @@ where
|
|||||||
impl<'a, Message> CodeEditor<'a, Message> {
|
impl<'a, Message> CodeEditor<'a, Message> {
|
||||||
fn emit_change(&self, shell: &mut Shell<'_, Message>) {
|
fn emit_change(&self, shell: &mut Shell<'_, Message>) {
|
||||||
if let Some(ref on_change) = self.on_change {
|
if let Some(ref on_change) = self.on_change {
|
||||||
let text = self.buffer.text();
|
let text = self.buffer.borrow().text();
|
||||||
shell.publish((on_change)(EditorMessage::ContentChanged(text)));
|
shell.publish((on_change)(EditorMessage::ContentChanged(text)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::collections::HashMap;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::widget::text_editor;
|
|
||||||
use iced::{Element, Subscription, Task};
|
use iced::{Element, Subscription, Task};
|
||||||
|
|
||||||
use bds_core::db::Database;
|
use bds_core::db::Database;
|
||||||
@@ -464,8 +463,12 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
// Extract content language from project metadata
|
// Extract content language from project metadata
|
||||||
if let Ok(meta) = engine::meta::read_project_json(&data_dir) {
|
if let Ok(meta) = engine::meta::read_project_json(&data_dir) {
|
||||||
self.content_language = meta.main_language.unwrap_or_else(|| "en".to_string());
|
let main_lang = meta.main_language.unwrap_or_else(|| "en".to_string());
|
||||||
|
self.content_language = main_lang.clone();
|
||||||
self.blog_languages = meta.blog_languages;
|
self.blog_languages = meta.blog_languages;
|
||||||
|
if !self.blog_languages.contains(&main_lang) {
|
||||||
|
self.blog_languages.insert(0, main_lang);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.refresh_counts();
|
self.refresh_counts();
|
||||||
@@ -487,8 +490,12 @@ impl BdsApp {
|
|||||||
if let Some(data_dir) = self.data_dir.clone() {
|
if let Some(data_dir) = self.data_dir.clone() {
|
||||||
let _ = engine::meta::startup_sync(&data_dir);
|
let _ = engine::meta::startup_sync(&data_dir);
|
||||||
if let Ok(meta) = engine::meta::read_project_json(&data_dir) {
|
if let Ok(meta) = engine::meta::read_project_json(&data_dir) {
|
||||||
self.content_language = meta.main_language.unwrap_or_else(|| "en".to_string());
|
let main_lang = meta.main_language.unwrap_or_else(|| "en".to_string());
|
||||||
|
self.content_language = main_lang.clone();
|
||||||
self.blog_languages = meta.blog_languages;
|
self.blog_languages = meta.blog_languages;
|
||||||
|
if !self.blog_languages.contains(&main_lang) {
|
||||||
|
self.blog_languages.insert(0, main_lang);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let name = self.active_project.as_ref().map(|p| p.name.clone()).unwrap_or_default();
|
let name = self.active_project.as_ref().map(|p| p.name.clone()).unwrap_or_default();
|
||||||
@@ -928,14 +935,10 @@ impl BdsApp {
|
|||||||
PostEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; }
|
PostEditorMsg::TitleChanged(s) => { state.title = s; state.is_dirty = true; }
|
||||||
PostEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; }
|
PostEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; }
|
||||||
PostEditorMsg::ExcerptChanged(s) => { state.excerpt = s; state.is_dirty = true; }
|
PostEditorMsg::ExcerptChanged(s) => { state.excerpt = s; state.is_dirty = true; }
|
||||||
PostEditorMsg::ContentAction(action) => {
|
PostEditorMsg::ContentChanged(new_text) => {
|
||||||
let is_edit = matches!(action, text_editor::Action::Edit(_));
|
state.content = new_text;
|
||||||
state.editor_content.perform(action);
|
|
||||||
if is_edit {
|
|
||||||
state.content = state.editor_content.text();
|
|
||||||
state.is_dirty = true;
|
state.is_dirty = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
PostEditorMsg::AuthorChanged(s) => { state.author = s; state.is_dirty = true; }
|
PostEditorMsg::AuthorChanged(s) => { state.author = s; state.is_dirty = true; }
|
||||||
PostEditorMsg::TemplateSlugChanged(s) => { state.template_slug = s; state.is_dirty = true; }
|
PostEditorMsg::TemplateSlugChanged(s) => { state.template_slug = s; state.is_dirty = true; }
|
||||||
PostEditorMsg::ToggleDoNotTranslate(b) => { state.do_not_translate = b; state.is_dirty = true; }
|
PostEditorMsg::ToggleDoNotTranslate(b) => { state.do_not_translate = b; state.is_dirty = true; }
|
||||||
@@ -1026,7 +1029,10 @@ impl BdsApp {
|
|||||||
TemplateEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; }
|
TemplateEditorMsg::SlugChanged(s) => { state.slug = s; state.is_dirty = true; }
|
||||||
TemplateEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; }
|
TemplateEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; }
|
||||||
TemplateEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; }
|
TemplateEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; }
|
||||||
TemplateEditorMsg::ContentChanged(s) => { state.content = s; state.is_dirty = true; }
|
TemplateEditorMsg::ContentChanged(new_text) => {
|
||||||
|
state.content = new_text;
|
||||||
|
state.is_dirty = true;
|
||||||
|
}
|
||||||
TemplateEditorMsg::Save => {
|
TemplateEditorMsg::Save => {
|
||||||
return self.save_template_editor(&tab_id);
|
return self.save_template_editor(&tab_id);
|
||||||
}
|
}
|
||||||
@@ -1065,9 +1071,9 @@ impl BdsApp {
|
|||||||
ScriptEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; }
|
ScriptEditorMsg::KindChanged(k) => { state.kind = k.0; state.is_dirty = true; }
|
||||||
ScriptEditorMsg::EntrypointChanged(s) => { state.entrypoint = s; state.is_dirty = true; }
|
ScriptEditorMsg::EntrypointChanged(s) => { state.entrypoint = s; state.is_dirty = true; }
|
||||||
ScriptEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; }
|
ScriptEditorMsg::EnabledChanged(b) => { state.enabled = b; state.is_dirty = true; }
|
||||||
ScriptEditorMsg::ContentChanged(s) => {
|
ScriptEditorMsg::ContentChanged(new_text) => {
|
||||||
state.discovered_entrypoints = engine::script::discover_entrypoints(&s);
|
state.discovered_entrypoints = engine::script::discover_entrypoints(&new_text);
|
||||||
state.content = s;
|
state.content = new_text;
|
||||||
state.is_dirty = true;
|
state.is_dirty = true;
|
||||||
}
|
}
|
||||||
ScriptEditorMsg::Save => {
|
ScriptEditorMsg::Save => {
|
||||||
@@ -2065,7 +2071,17 @@ impl BdsApp {
|
|||||||
TabType::Templates => {
|
TabType::Templates => {
|
||||||
if !self.template_editors.contains_key(&tab.id) {
|
if !self.template_editors.contains_key(&tab.id) {
|
||||||
match bds_core::db::queries::template::get_template_by_id(db.conn(), &tab.id) {
|
match bds_core::db::queries::template::get_template_by_id(db.conn(), &tab.id) {
|
||||||
Ok(template) => {
|
Ok(mut template) => {
|
||||||
|
// Published templates: read content from file
|
||||||
|
if template.content.is_none() {
|
||||||
|
if let Some(ref data_dir) = self.data_dir {
|
||||||
|
let rel = bds_core::util::paths::template_file_path(&template.slug);
|
||||||
|
let path = data_dir.join(&rel);
|
||||||
|
if let Ok(body) = std::fs::read_to_string(&path) {
|
||||||
|
template.content = Some(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.template_editors.insert(template.id.clone(), TemplateEditorState::from_template(&template));
|
self.template_editors.insert(template.id.clone(), TemplateEditorState::from_template(&template));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2077,7 +2093,17 @@ impl BdsApp {
|
|||||||
TabType::Scripts => {
|
TabType::Scripts => {
|
||||||
if !self.script_editors.contains_key(&tab.id) {
|
if !self.script_editors.contains_key(&tab.id) {
|
||||||
match bds_core::db::queries::script::get_script_by_id(db.conn(), &tab.id) {
|
match bds_core::db::queries::script::get_script_by_id(db.conn(), &tab.id) {
|
||||||
Ok(script) => {
|
Ok(mut script) => {
|
||||||
|
// Published scripts: read content from file
|
||||||
|
if script.content.is_none() {
|
||||||
|
if let Some(ref data_dir) = self.data_dir {
|
||||||
|
let rel = bds_core::util::paths::script_file_path(&script.slug);
|
||||||
|
let path = data_dir.join(&rel);
|
||||||
|
if let Ok(body) = std::fs::read_to_string(&path) {
|
||||||
|
script.content = Some(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.script_editors.insert(script.id.clone(), ScriptEditorState::from_script(&script));
|
self.script_editors.insert(script.id.clone(), ScriptEditorState::from_script(&script));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2096,7 +2122,29 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TabType::Settings => {
|
TabType::Settings => {
|
||||||
// Settings state will be initialized lazily
|
if self.settings_state.is_none() {
|
||||||
|
let mut state = SettingsViewState::default();
|
||||||
|
if let Some(ref project) = self.active_project {
|
||||||
|
state.project_name = project.name.clone();
|
||||||
|
state.project_description = project.description.clone().unwrap_or_default();
|
||||||
|
state.data_path = project.data_path.clone().unwrap_or_default();
|
||||||
|
}
|
||||||
|
if let Some(ref data_dir) = self.data_dir {
|
||||||
|
if let Ok(meta) = engine::meta::read_project_json(data_dir) {
|
||||||
|
state.public_url = meta.public_url.unwrap_or_default();
|
||||||
|
state.default_author = meta.default_author.unwrap_or_default();
|
||||||
|
state.max_posts_per_page = meta.max_posts_per_page.to_string();
|
||||||
|
}
|
||||||
|
if let Ok(pub_prefs) = engine::meta::read_publishing_json(data_dir) {
|
||||||
|
state.ssh_host = pub_prefs.ssh_host.unwrap_or_default();
|
||||||
|
state.ssh_username = pub_prefs.ssh_user.unwrap_or_default();
|
||||||
|
state.ssh_remote_path = pub_prefs.ssh_remote_path.unwrap_or_default();
|
||||||
|
state.ssh_mode = format!("{:?}", pub_prefs.ssh_mode).to_lowercase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.offline_mode = self.offline_mode;
|
||||||
|
self.settings_state = Some(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, text_editor, Space};
|
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||||
use iced::widget::text::{Shaping, Wrapping};
|
use iced::widget::text::{Shaping, Wrapping};
|
||||||
use iced::{Color, Element, Length, Theme};
|
use iced::{Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::{self, UiLocale};
|
use bds_core::i18n::{self, UiLocale};
|
||||||
use bds_core::model::{Post, PostStatus, PostTranslation};
|
use bds_core::model::{Post, PostStatus, PostTranslation};
|
||||||
|
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::inputs;
|
||||||
@@ -37,7 +39,7 @@ pub struct PostEditorState {
|
|||||||
pub slug: String,
|
pub slug: String,
|
||||||
pub excerpt: String,
|
pub excerpt: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub editor_content: text_editor::Content,
|
pub editor_buffer: RefCell<EditorBuffer>,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
@@ -83,7 +85,7 @@ impl Clone for PostEditorState {
|
|||||||
slug: self.slug.clone(),
|
slug: self.slug.clone(),
|
||||||
excerpt: self.excerpt.clone(),
|
excerpt: self.excerpt.clone(),
|
||||||
content: self.content.clone(),
|
content: self.content.clone(),
|
||||||
editor_content: text_editor::Content::with_text(&self.content),
|
editor_buffer: RefCell::new(EditorBuffer::new(&self.content)),
|
||||||
tags: self.tags.clone(),
|
tags: self.tags.clone(),
|
||||||
categories: self.categories.clone(),
|
categories: self.categories.clone(),
|
||||||
author: self.author.clone(),
|
author: self.author.clone(),
|
||||||
@@ -133,7 +135,7 @@ impl PostEditorState {
|
|||||||
slug: post.slug.clone(),
|
slug: post.slug.clone(),
|
||||||
excerpt: post.excerpt.clone().unwrap_or_default(),
|
excerpt: post.excerpt.clone().unwrap_or_default(),
|
||||||
content: content.clone(),
|
content: content.clone(),
|
||||||
editor_content: text_editor::Content::with_text(&content),
|
editor_buffer: RefCell::new(EditorBuffer::new(&content)),
|
||||||
tags: post.tags.clone(),
|
tags: post.tags.clone(),
|
||||||
categories: post.categories.clone(),
|
categories: post.categories.clone(),
|
||||||
author: post.author.clone().unwrap_or_default(),
|
author: post.author.clone().unwrap_or_default(),
|
||||||
@@ -192,7 +194,7 @@ impl PostEditorState {
|
|||||||
self.title = saved.title;
|
self.title = saved.title;
|
||||||
self.excerpt = saved.excerpt;
|
self.excerpt = saved.excerpt;
|
||||||
self.content = saved.content.clone();
|
self.content = saved.content.clone();
|
||||||
self.editor_content = text_editor::Content::with_text(&saved.content);
|
self.editor_buffer = RefCell::new(EditorBuffer::new(&saved.content));
|
||||||
self.status = saved.status;
|
self.status = saved.status;
|
||||||
self.is_dirty = saved.is_dirty;
|
self.is_dirty = saved.is_dirty;
|
||||||
}
|
}
|
||||||
@@ -201,14 +203,14 @@ impl PostEditorState {
|
|||||||
self.title = draft.title.clone();
|
self.title = draft.title.clone();
|
||||||
self.excerpt = draft.excerpt.clone();
|
self.excerpt = draft.excerpt.clone();
|
||||||
self.content = draft.content.clone();
|
self.content = draft.content.clone();
|
||||||
self.editor_content = text_editor::Content::with_text(&draft.content);
|
self.editor_buffer = RefCell::new(EditorBuffer::new(&draft.content));
|
||||||
self.is_dirty = draft.is_dirty;
|
self.is_dirty = draft.is_dirty;
|
||||||
} else {
|
} else {
|
||||||
// No translation yet — blank fields
|
// No translation yet — blank fields
|
||||||
self.title = String::new();
|
self.title = String::new();
|
||||||
self.excerpt = String::new();
|
self.excerpt = String::new();
|
||||||
self.content = String::new();
|
self.content = String::new();
|
||||||
self.editor_content = text_editor::Content::with_text("");
|
self.editor_buffer = RefCell::new(EditorBuffer::new(""));
|
||||||
self.is_dirty = false;
|
self.is_dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +264,7 @@ pub enum PostEditorMsg {
|
|||||||
TitleChanged(String),
|
TitleChanged(String),
|
||||||
SlugChanged(String),
|
SlugChanged(String),
|
||||||
ExcerptChanged(String),
|
ExcerptChanged(String),
|
||||||
ContentAction(text_editor::Action),
|
ContentChanged(String),
|
||||||
AuthorChanged(String),
|
AuthorChanged(String),
|
||||||
TemplateSlugChanged(String),
|
TemplateSlugChanged(String),
|
||||||
ToggleDoNotTranslate(bool),
|
ToggleDoNotTranslate(bool),
|
||||||
@@ -461,13 +463,17 @@ pub fn view<'a>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Content section (fills remaining space) ──
|
// ── Content section (fills remaining space) ──
|
||||||
let content_placeholder = t(locale, "editor.contentPlaceholder");
|
|
||||||
let content_label = inputs::section_header(&t(locale, "editor.content"));
|
let content_label = inputs::section_header(&t(locale, "editor.content"));
|
||||||
let editor_widget = text_editor(&state.editor_content)
|
let editor_widget: Element<'a, Message> = CodeEditor::new(
|
||||||
.placeholder(content_placeholder)
|
&state.editor_buffer,
|
||||||
.on_action(|action| Message::PostEditor(PostEditorMsg::ContentAction(action)))
|
highlighter(),
|
||||||
.height(Length::Fill)
|
"md",
|
||||||
.style(editor_style);
|
)
|
||||||
|
.on_change(|msg| match msg {
|
||||||
|
EditorMessage::ContentChanged(s) => Message::PostEditor(PostEditorMsg::ContentChanged(s)),
|
||||||
|
EditorMessage::SaveRequested => Message::PostEditor(PostEditorMsg::Save),
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
|
||||||
// ── Footer ──
|
// ── Footer ──
|
||||||
let footer = row![
|
let footer = row![
|
||||||
@@ -505,28 +511,6 @@ pub fn view<'a>(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dark editor style for the text_editor widget.
|
|
||||||
fn editor_style(_theme: &Theme, status: text_editor::Status) -> text_editor::Style {
|
|
||||||
let bg = Color::from_rgb(0.10, 0.11, 0.14);
|
|
||||||
let border_color = match status {
|
|
||||||
text_editor::Status::Focused => Color::from_rgb(0.30, 0.50, 0.80),
|
|
||||||
text_editor::Status::Hovered => Color::from_rgb(0.30, 0.32, 0.40),
|
|
||||||
_ => Color::from_rgb(0.22, 0.24, 0.30),
|
|
||||||
};
|
|
||||||
text_editor::Style {
|
|
||||||
background: iced::Background::Color(bg),
|
|
||||||
border: iced::Border {
|
|
||||||
radius: 4.0.into(),
|
|
||||||
width: 1.0,
|
|
||||||
color: border_color,
|
|
||||||
},
|
|
||||||
icon: Color::from_rgb(0.50, 0.52, 0.58),
|
|
||||||
placeholder: Color::from_rgb(0.40, 0.42, 0.48),
|
|
||||||
value: Color::from_rgb(0.85, 0.87, 0.92),
|
|
||||||
selection: Color::from_rgba(0.30, 0.50, 0.80, 0.40),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Chip input: shows existing chips as removable buttons + a text input to add new ones.
|
/// Chip input: shows existing chips as removable buttons + a text input to add new ones.
|
||||||
fn chip_input_field<'a>(
|
fn chip_input_field<'a>(
|
||||||
label: &str,
|
label: &str,
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||||
use iced::{Color, Element, Length, Theme};
|
use iced::{Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
use bds_core::model::{Script, ScriptKind, ScriptStatus};
|
use bds_core::model::{Script, ScriptKind, ScriptStatus};
|
||||||
|
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::inputs;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
|
|
||||||
/// State for an open script editor.
|
/// State for an open script editor.
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ScriptEditorState {
|
pub struct ScriptEditorState {
|
||||||
pub script_id: String,
|
pub script_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -18,6 +20,7 @@ pub struct ScriptEditorState {
|
|||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub editor_buffer: RefCell<EditorBuffer>,
|
||||||
pub status: ScriptStatus,
|
pub status: ScriptStatus,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
@@ -27,6 +30,37 @@ pub struct ScriptEditorState {
|
|||||||
pub is_dirty: bool,
|
pub is_dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ScriptEditorState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ScriptEditorState")
|
||||||
|
.field("script_id", &self.script_id)
|
||||||
|
.field("title", &self.title)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for ScriptEditorState {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
script_id: self.script_id.clone(),
|
||||||
|
title: self.title.clone(),
|
||||||
|
slug: self.slug.clone(),
|
||||||
|
kind: self.kind.clone(),
|
||||||
|
entrypoint: self.entrypoint.clone(),
|
||||||
|
enabled: self.enabled,
|
||||||
|
content: self.content.clone(),
|
||||||
|
editor_buffer: RefCell::new(EditorBuffer::new(&self.content)),
|
||||||
|
status: self.status.clone(),
|
||||||
|
version: self.version,
|
||||||
|
created_at: self.created_at,
|
||||||
|
updated_at: self.updated_at,
|
||||||
|
discovered_entrypoints: self.discovered_entrypoints.clone(),
|
||||||
|
validation_error: self.validation_error.clone(),
|
||||||
|
is_dirty: self.is_dirty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ScriptEditorState {
|
impl ScriptEditorState {
|
||||||
pub fn from_script(script: &Script) -> Self {
|
pub fn from_script(script: &Script) -> Self {
|
||||||
let content = script.content.clone().unwrap_or_default();
|
let content = script.content.clone().unwrap_or_default();
|
||||||
@@ -38,7 +72,8 @@ impl ScriptEditorState {
|
|||||||
kind: script.kind.clone(),
|
kind: script.kind.clone(),
|
||||||
entrypoint: script.entrypoint.clone(),
|
entrypoint: script.entrypoint.clone(),
|
||||||
enabled: script.enabled,
|
enabled: script.enabled,
|
||||||
content,
|
content: content.clone(),
|
||||||
|
editor_buffer: RefCell::new(EditorBuffer::new(&content)),
|
||||||
status: script.status.clone(),
|
status: script.status.clone(),
|
||||||
version: script.version,
|
version: script.version,
|
||||||
created_at: script.created_at,
|
created_at: script.created_at,
|
||||||
@@ -157,16 +192,20 @@ pub fn view<'a>(
|
|||||||
.spacing(16)
|
.spacing(16)
|
||||||
.width(Length::Fill);
|
.width(Length::Fill);
|
||||||
|
|
||||||
// Content editor (placeholder — will use CodeEditor with Lua syntax)
|
// Content editor (CodeEditor with Lua syntax highlighting)
|
||||||
let content_section: Element<'a, Message> = column![
|
let content_section: Element<'a, Message> = column![
|
||||||
inputs::section_header(&t(locale, "editor.content")),
|
inputs::section_header(&t(locale, "editor.content")),
|
||||||
container(
|
Element::from(
|
||||||
text_input("", &state.content)
|
CodeEditor::new(
|
||||||
.on_input(|s| Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)))
|
&state.editor_buffer,
|
||||||
.size(14)
|
highlighter(),
|
||||||
|
"lua",
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.on_change(|msg| match msg {
|
||||||
.height(Length::Fixed(300.0)),
|
EditorMessage::ContentChanged(s) => Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)),
|
||||||
|
EditorMessage::SaveRequested => Message::ScriptEditor(ScriptEditorMsg::Save),
|
||||||
|
})
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
@@ -193,21 +232,28 @@ pub fn view<'a>(
|
|||||||
]
|
]
|
||||||
.padding(8);
|
.padding(8);
|
||||||
|
|
||||||
let body = scrollable(
|
// Top pane: header + metadata (scrollable for overflow)
|
||||||
|
let top_pane = scrollable(
|
||||||
column![
|
column![
|
||||||
header,
|
header,
|
||||||
meta_row1,
|
meta_row1,
|
||||||
meta_row2,
|
meta_row2,
|
||||||
content_section,
|
|
||||||
validation,
|
|
||||||
footer,
|
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
);
|
)
|
||||||
|
.height(Length::Shrink);
|
||||||
|
|
||||||
container(body)
|
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
|
||||||
|
column![
|
||||||
|
top_pane,
|
||||||
|
content_section,
|
||||||
|
validation,
|
||||||
|
footer,
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.padding([0, 16])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let cloud = iced::widget::Column::with_children(chips).spacing(6);
|
let cloud = row(chips).spacing(6).wrap();
|
||||||
|
|
||||||
scrollable(
|
scrollable(
|
||||||
container(cloud)
|
container(cloud)
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
||||||
use iced::{Color, Element, Length, Theme};
|
use iced::{Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
use bds_core::model::{Template, TemplateKind, TemplateStatus};
|
use bds_core::model::{Template, TemplateKind, TemplateStatus};
|
||||||
|
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::components::inputs;
|
use crate::components::inputs;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
|
|
||||||
/// State for an open template editor.
|
/// State for an open template editor.
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TemplateEditorState {
|
pub struct TemplateEditorState {
|
||||||
pub template_id: String,
|
pub template_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -17,6 +19,7 @@ pub struct TemplateEditorState {
|
|||||||
pub kind: TemplateKind,
|
pub kind: TemplateKind,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub editor_buffer: RefCell<EditorBuffer>,
|
||||||
pub status: TemplateStatus,
|
pub status: TemplateStatus,
|
||||||
pub version: i32,
|
pub version: i32,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
@@ -25,15 +28,46 @@ pub struct TemplateEditorState {
|
|||||||
pub is_dirty: bool,
|
pub is_dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for TemplateEditorState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("TemplateEditorState")
|
||||||
|
.field("template_id", &self.template_id)
|
||||||
|
.field("title", &self.title)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for TemplateEditorState {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
template_id: self.template_id.clone(),
|
||||||
|
title: self.title.clone(),
|
||||||
|
slug: self.slug.clone(),
|
||||||
|
kind: self.kind.clone(),
|
||||||
|
enabled: self.enabled,
|
||||||
|
content: self.content.clone(),
|
||||||
|
editor_buffer: RefCell::new(EditorBuffer::new(&self.content)),
|
||||||
|
status: self.status.clone(),
|
||||||
|
version: self.version,
|
||||||
|
created_at: self.created_at,
|
||||||
|
updated_at: self.updated_at,
|
||||||
|
validation_error: self.validation_error.clone(),
|
||||||
|
is_dirty: self.is_dirty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TemplateEditorState {
|
impl TemplateEditorState {
|
||||||
pub fn from_template(tpl: &Template) -> Self {
|
pub fn from_template(tpl: &Template) -> Self {
|
||||||
|
let content = tpl.content.clone().unwrap_or_default();
|
||||||
Self {
|
Self {
|
||||||
template_id: tpl.id.clone(),
|
template_id: tpl.id.clone(),
|
||||||
title: tpl.title.clone(),
|
title: tpl.title.clone(),
|
||||||
slug: tpl.slug.clone(),
|
slug: tpl.slug.clone(),
|
||||||
kind: tpl.kind.clone(),
|
kind: tpl.kind.clone(),
|
||||||
enabled: tpl.enabled,
|
enabled: tpl.enabled,
|
||||||
content: tpl.content.clone().unwrap_or_default(),
|
content: content.clone(),
|
||||||
|
editor_buffer: RefCell::new(EditorBuffer::new(&content)),
|
||||||
status: tpl.status.clone(),
|
status: tpl.status.clone(),
|
||||||
version: tpl.version,
|
version: tpl.version,
|
||||||
created_at: tpl.created_at,
|
created_at: tpl.created_at,
|
||||||
@@ -136,16 +170,20 @@ pub fn view<'a>(
|
|||||||
);
|
);
|
||||||
let meta_row2 = row![kind_select, enabled_check].spacing(16).width(Length::Fill);
|
let meta_row2 = row![kind_select, enabled_check].spacing(16).width(Length::Fill);
|
||||||
|
|
||||||
// Content editor (text area placeholder — will use CodeEditor widget for liquid)
|
// Content editor (CodeEditor with Liquid/HTML syntax highlighting)
|
||||||
let content_section: Element<'a, Message> = column![
|
let content_section: Element<'a, Message> = column![
|
||||||
inputs::section_header(&t(locale, "editor.content")),
|
inputs::section_header(&t(locale, "editor.content")),
|
||||||
container(
|
Element::from(
|
||||||
text_input("", &state.content)
|
CodeEditor::new(
|
||||||
.on_input(|s| Message::TemplateEditor(TemplateEditorMsg::ContentChanged(s)))
|
&state.editor_buffer,
|
||||||
.size(14)
|
highlighter(),
|
||||||
|
"liquid",
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.on_change(|msg| match msg {
|
||||||
.height(Length::Fixed(300.0)),
|
EditorMessage::ContentChanged(s) => Message::TemplateEditor(TemplateEditorMsg::ContentChanged(s)),
|
||||||
|
EditorMessage::SaveRequested => Message::TemplateEditor(TemplateEditorMsg::Save),
|
||||||
|
})
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
@@ -172,21 +210,28 @@ pub fn view<'a>(
|
|||||||
]
|
]
|
||||||
.padding(8);
|
.padding(8);
|
||||||
|
|
||||||
let body = scrollable(
|
// Top pane: header + metadata (scrollable for overflow)
|
||||||
|
let top_pane = scrollable(
|
||||||
column![
|
column![
|
||||||
header,
|
header,
|
||||||
meta_row1,
|
meta_row1,
|
||||||
meta_row2,
|
meta_row2,
|
||||||
content_section,
|
|
||||||
validation,
|
|
||||||
footer,
|
|
||||||
]
|
]
|
||||||
.spacing(12)
|
.spacing(12)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
);
|
)
|
||||||
|
.height(Length::Shrink);
|
||||||
|
|
||||||
container(body)
|
// Full layout: top pane (shrink), content (fill), validation + footer (shrink)
|
||||||
|
column![
|
||||||
|
top_pane,
|
||||||
|
content_section,
|
||||||
|
validation,
|
||||||
|
footer,
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.padding([0, 16])
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
Reference in New Issue
Block a user