use azul_core::{
dom::{DomId, NodeId},
selection::{CursorAffinity, GraphemeClusterId, TextCursor},
task::Instant,
};
pub const CURSOR_BLINK_INTERVAL_MS: u64 = 530;
#[derive(Debug, Clone)]
pub struct CursorManager {
pub cursor: Option<TextCursor>,
pub cursor_location: Option<CursorLocation>,
pub is_visible: bool,
pub last_input_time: Option<Instant>,
pub blink_timer_active: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CursorLocation {
pub dom_id: DomId,
pub node_id: NodeId,
pub contenteditable_key: u64,
}
impl CursorLocation {
pub fn new(dom_id: DomId, node_id: NodeId) -> Self {
Self {
dom_id,
node_id,
contenteditable_key: 0,
}
}
pub fn with_key(dom_id: DomId, node_id: NodeId, contenteditable_key: u64) -> Self {
Self {
dom_id,
node_id,
contenteditable_key,
}
}
}
impl PartialEq for CursorManager {
fn eq(&self, other: &Self) -> bool {
self.cursor == other.cursor && self.cursor_location == other.cursor_location
}
}
impl Default for CursorManager {
fn default() -> Self {
Self::new()
}
}
impl CursorManager {
pub fn new() -> Self {
Self {
cursor: None,
cursor_location: None,
is_visible: false,
last_input_time: None,
blink_timer_active: false,
}
}
pub fn get_cursor(&self) -> Option<&TextCursor> {
self.cursor.as_ref()
}
pub fn get_cursor_location(&self) -> Option<&CursorLocation> {
self.cursor_location.as_ref()
}
pub fn set_cursor(&mut self, cursor: Option<TextCursor>, location: Option<CursorLocation>) {
self.cursor = cursor;
self.cursor_location = location;
if cursor.is_some() {
self.is_visible = true;
}
}
pub fn set_cursor_with_time(&mut self, cursor: Option<TextCursor>, location: Option<CursorLocation>, now: Instant) {
self.cursor = cursor;
self.cursor_location = location;
if cursor.is_some() {
self.is_visible = true;
self.last_input_time = Some(now);
}
}
pub fn clear(&mut self) {
self.cursor = None;
self.cursor_location = None;
self.is_visible = false;
self.last_input_time = None;
self.blink_timer_active = false;
}
pub fn has_cursor(&self) -> bool {
self.cursor.is_some()
}
pub fn should_draw_cursor(&self) -> bool {
self.cursor.is_some() && self.is_visible
}
pub fn reset_blink_on_input(&mut self, now: Instant) {
self.is_visible = true;
self.last_input_time = Some(now);
}
pub fn toggle_visibility(&mut self) -> bool {
self.is_visible = !self.is_visible;
self.is_visible
}
pub fn set_visibility(&mut self, visible: bool) {
self.is_visible = visible;
}
pub fn should_blink(&self, now: &Instant) -> bool {
use azul_core::task::{Duration, SystemTimeDiff};
match &self.last_input_time {
Some(last_input) => {
let elapsed = now.duration_since(last_input);
let blink_interval = Duration::System(SystemTimeDiff::from_millis(CURSOR_BLINK_INTERVAL_MS));
elapsed.greater_than(&blink_interval)
}
None => true, }
}
pub fn set_blink_timer_active(&mut self, active: bool) {
self.blink_timer_active = active;
}
pub fn is_blink_timer_active(&self) -> bool {
self.blink_timer_active
}
pub fn initialize_cursor_at_end(
&mut self,
dom_id: DomId,
node_id: NodeId,
text_layout: Option<&alloc::sync::Arc<crate::text3::cache::UnifiedLayout>>,
) -> bool {
let Some(layout) = text_layout else {
self.cursor = Some(TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Trailing,
});
self.cursor_location = Some(CursorLocation::new(dom_id, node_id));
self.is_visible = true; return true;
};
let mut last_cluster_id: Option<GraphemeClusterId> = None;
for item in layout.items.iter().rev() {
if let crate::text3::cache::ShapedItem::Cluster(cluster) = &item.item {
last_cluster_id = Some(cluster.source_cluster_id);
break;
}
}
self.cursor = Some(TextCursor {
cluster_id: last_cluster_id.unwrap_or(GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
}),
affinity: CursorAffinity::Trailing,
});
self.cursor_location = Some(CursorLocation::new(dom_id, node_id));
self.is_visible = true;
true
}
pub fn initialize_cursor_at_start(&mut self, dom_id: DomId, node_id: NodeId) {
self.cursor = Some(TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: 0,
},
affinity: CursorAffinity::Trailing,
});
self.cursor_location = Some(CursorLocation::new(dom_id, node_id));
}
pub fn move_cursor_to(&mut self, cursor: TextCursor, dom_id: DomId, node_id: NodeId) {
self.cursor = Some(cursor);
self.cursor_location = Some(CursorLocation::new(dom_id, node_id));
}
pub fn is_cursor_in_node(&self, dom_id: DomId, node_id: NodeId) -> bool {
self.cursor_location
.as_ref()
.map(|loc| loc.dom_id == dom_id && loc.node_id == node_id)
.unwrap_or(false)
}
pub fn get_cursor_node(&self) -> Option<azul_core::dom::DomNodeId> {
self.cursor_location.as_ref().map(|loc| {
azul_core::dom::DomNodeId {
dom: loc.dom_id,
node: azul_core::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(loc.node_id)),
}
})
}
pub fn update_node_id(&mut self, new_node: azul_core::dom::DomNodeId) {
if let Some(ref mut loc) = self.cursor_location {
if let Some(new_id) = new_node.node.into_crate_internal() {
loc.dom_id = new_node.dom;
loc.node_id = new_id;
}
}
}
pub fn remap_node_ids(
&mut self,
dom_id: DomId,
node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
) {
if let Some(ref mut loc) = self.cursor_location {
if loc.dom_id == dom_id {
if let Some(&new_node_id) = node_id_map.get(&loc.node_id) {
loc.node_id = new_node_id;
} else {
self.cursor_location = None;
}
}
}
}
}