use azul_core::{
dom::DomNodeId,
geom::LogicalPosition,
selection::{OptionSelectionRange, SelectionRange},
task::Instant,
window::CursorPosition,
};
use azul_css::AzString;
use crate::managers::selection::ClipboardContent;
pub type ChangesetId = usize;
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextChangeset {
pub id: ChangesetId,
pub target: DomNodeId,
pub operation: TextOperation,
pub timestamp: Instant,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpInsertText {
pub position: CursorPosition,
pub text: AzString,
pub new_cursor: CursorPosition,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpDeleteText {
pub range: SelectionRange,
pub deleted_text: AzString,
pub new_cursor: CursorPosition,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpReplaceText {
pub range: SelectionRange,
pub old_text: AzString,
pub new_text: AzString,
pub new_cursor: CursorPosition,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpSetSelection {
pub old_range: OptionSelectionRange,
pub new_range: SelectionRange,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpExtendSelection {
pub old_range: SelectionRange,
pub new_range: SelectionRange,
pub direction: SelectionDirection,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpClearSelection {
pub old_range: SelectionRange,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpMoveCursor {
pub old_position: CursorPosition,
pub new_position: CursorPosition,
pub movement: CursorMovement,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpCopy {
pub range: SelectionRange,
pub content: ClipboardContent,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpCut {
pub range: SelectionRange,
pub content: ClipboardContent,
pub new_cursor: CursorPosition,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpPaste {
pub position: CursorPosition,
pub content: ClipboardContent,
pub new_cursor: CursorPosition,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct TextOpSelectAll {
pub old_range: OptionSelectionRange,
pub new_range: SelectionRange,
}
#[derive(Debug, Clone)]
#[repr(C, u8)]
pub enum TextOperation {
InsertText(TextOpInsertText),
DeleteText(TextOpDeleteText),
ReplaceText(TextOpReplaceText),
SetSelection(TextOpSetSelection),
ExtendSelection(TextOpExtendSelection),
ClearSelection(TextOpClearSelection),
MoveCursor(TextOpMoveCursor),
Copy(TextOpCopy),
Cut(TextOpCut),
Paste(TextOpPaste),
SelectAll(TextOpSelectAll),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum SelectionDirection {
Forward,
Backward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum CursorMovement {
Left,
Right,
Up,
Down,
WordLeft,
WordRight,
LineStart,
LineEnd,
DocumentStart,
DocumentEnd,
Absolute,
}
impl TextChangeset {
pub fn new(target: DomNodeId, operation: TextOperation, timestamp: Instant) -> Self {
use std::sync::atomic::{AtomicUsize, Ordering};
static CHANGESET_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
Self {
id: CHANGESET_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
target,
operation,
timestamp,
}
}
pub fn mutates_text(&self) -> bool {
matches!(
self.operation,
TextOperation::InsertText { .. }
| TextOperation::DeleteText { .. }
| TextOperation::ReplaceText { .. }
| TextOperation::Cut { .. }
| TextOperation::Paste { .. }
)
}
pub fn changes_selection(&self) -> bool {
matches!(
self.operation,
TextOperation::SetSelection { .. }
| TextOperation::ExtendSelection { .. }
| TextOperation::ClearSelection { .. }
| TextOperation::MoveCursor { .. }
| TextOperation::SelectAll { .. }
)
}
pub fn uses_clipboard(&self) -> bool {
matches!(
self.operation,
TextOperation::Copy { .. } | TextOperation::Cut { .. } | TextOperation::Paste { .. }
)
}
pub fn resulting_cursor_position(&self) -> Option<CursorPosition> {
match &self.operation {
TextOperation::InsertText(op) => Some(op.new_cursor),
TextOperation::DeleteText(op) => Some(op.new_cursor),
TextOperation::ReplaceText(op) => Some(op.new_cursor),
TextOperation::Cut(op) => Some(op.new_cursor),
TextOperation::Paste(op) => Some(op.new_cursor),
TextOperation::MoveCursor(op) => Some(op.new_position),
_ => None,
}
}
pub fn resulting_selection_range(&self) -> Option<SelectionRange> {
match &self.operation {
TextOperation::SetSelection(op) => Some(op.new_range),
TextOperation::ExtendSelection(op) => Some(op.new_range),
TextOperation::SelectAll(op) => Some(op.new_range),
_ => None,
}
}
}
fn get_current_time() -> Instant {
let external = crate::callbacks::ExternalSystemCallbacks::rust_internal();
(external.get_system_time_fn.cb)().into()
}
pub fn create_copy_changeset(
target: DomNodeId,
timestamp: Instant,
layout_window: &crate::window::LayoutWindow,
) -> Option<TextChangeset> {
let dom_id = &target.dom;
let content = layout_window.get_selected_content_for_clipboard(dom_id)?;
let ranges = layout_window.selection_manager.get_ranges(dom_id);
let range = ranges.first()?;
Some(TextChangeset::new(
target,
TextOperation::Copy(TextOpCopy {
range: *range,
content,
}),
timestamp,
))
}
pub fn create_cut_changeset(
target: DomNodeId,
timestamp: Instant,
layout_window: &crate::window::LayoutWindow,
) -> Option<TextChangeset> {
let dom_id = &target.dom;
let content = layout_window.get_selected_content_for_clipboard(dom_id)?;
let ranges = layout_window.selection_manager.get_ranges(dom_id);
let range = ranges.first()?;
let new_cursor_position = azul_core::window::CursorPosition::Uninitialized;
Some(TextChangeset::new(
target,
TextOperation::Cut(TextOpCut {
range: *range,
content,
new_cursor: new_cursor_position,
}),
timestamp,
))
}
pub fn create_paste_changeset(
target: DomNodeId,
timestamp: Instant,
layout_window: &crate::window::LayoutWindow,
) -> Option<TextChangeset> {
None
}
pub fn create_select_all_changeset(
target: DomNodeId,
timestamp: Instant,
layout_window: &crate::window::LayoutWindow,
) -> Option<TextChangeset> {
use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
let dom_id = &target.dom;
let node_id = target.node.into_crate_internal()?;
let old_range = layout_window
.selection_manager
.get_ranges(dom_id)
.first()
.copied();
let content = layout_window.get_text_before_textinput(*dom_id, node_id);
let text = layout_window.extract_text_from_inline_content(&content);
let start_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Leading,
};
let end_cursor = TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: text.len() as u32,
},
affinity: CursorAffinity::Leading,
};
let new_range = azul_core::selection::SelectionRange {
start: start_cursor,
end: end_cursor,
};
Some(TextChangeset::new(
target,
TextOperation::SelectAll(TextOpSelectAll {
old_range: old_range.into(),
new_range,
}),
timestamp,
))
}
pub fn create_delete_selection_changeset(
target: DomNodeId,
forward: bool,
timestamp: Instant,
layout_window: &crate::window::LayoutWindow,
) -> Option<TextChangeset> {
use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
let dom_id = &target.dom;
let node_id = target.node.into_crate_internal()?;
let ranges = layout_window.selection_manager.get_ranges(dom_id);
let cursor = layout_window.cursor_manager.get_cursor();
let (delete_range, deleted_text) = if let Some(range) = ranges.first() {
let content = layout_window.get_text_before_textinput(*dom_id, node_id);
let text = layout_window.extract_text_from_inline_content(&content);
let deleted = String::new();
(*range, deleted)
} else if let Some(cursor_pos) = cursor {
let content = layout_window.get_text_before_textinput(*dom_id, node_id);
let text = layout_window.extract_text_from_inline_content(&content);
let byte_pos = cursor_pos.cluster_id.start_byte_in_run as usize;
let (range, deleted) = if forward {
if byte_pos >= text.len() {
return None; }
let end_pos = (byte_pos + 1).min(text.len());
let deleted = text[byte_pos..end_pos].to_string();
let range = azul_core::selection::SelectionRange {
start: *cursor_pos,
end: TextCursor {
cluster_id: GraphemeClusterId {
source_run: cursor_pos.cluster_id.source_run,
start_byte_in_run: end_pos as u32,
},
affinity: CursorAffinity::Leading,
},
};
(range, deleted)
} else {
if byte_pos == 0 {
return None; }
let start_pos = byte_pos.saturating_sub(1);
let deleted = text[start_pos..byte_pos].to_string();
let range = azul_core::selection::SelectionRange {
start: TextCursor {
cluster_id: GraphemeClusterId {
source_run: cursor_pos.cluster_id.source_run,
start_byte_in_run: start_pos as u32,
},
affinity: CursorAffinity::Leading,
},
end: *cursor_pos,
};
(range, deleted)
};
(range, deleted)
} else {
return None; };
let new_cursor = azul_core::window::CursorPosition::Uninitialized;
Some(TextChangeset::new(
target,
TextOperation::DeleteText(TextOpDeleteText {
range: delete_range,
deleted_text: deleted_text.into(),
new_cursor,
}),
timestamp,
))
}