123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- use std::{collections::HashMap, io::stdout};
- use crossterm::{cursor::MoveTo, execute};
- use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData};
- use dioxus_native_core::{
- custom_element::CustomElement,
- node::OwnedAttributeDiscription,
- node_ref::AttributeMask,
- prelude::{ElementNode, NodeType},
- real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
- utils::cursor::{Cursor, Pos},
- NodeId,
- };
- use shipyard::UniqueView;
- use taffy::geometry::Point;
- use crate::{query::get_layout, Event, EventData, FormData, Query};
- use super::{RinkWidget, WidgetContext};
- pub(crate) trait TextLikeController {
- fn display_text(&self, text: &str) -> String {
- text.to_string()
- }
- }
- #[derive(Debug, Default)]
- pub(crate) struct EmptyController;
- impl TextLikeController for EmptyController {}
- #[derive(Debug, Default)]
- pub(crate) struct TextLike<C: TextLikeController = EmptyController> {
- text: String,
- div_wrapper: NodeId,
- pre_cursor_text: NodeId,
- highlighted_text: NodeId,
- post_cursor_text: NodeId,
- cursor: Cursor,
- dragging: bool,
- border: bool,
- max_len: Option<usize>,
- controller: C,
- }
- impl<C: TextLikeController> TextLike<C> {
- fn width(el: &ElementNodeMut) -> String {
- if let Some(value) = el
- .get_attribute(&OwnedAttributeDiscription {
- name: "width".to_string(),
- namespace: Some("style".to_string()),
- })
- .and_then(|value| value.as_text())
- .map(|value| value.to_string())
- {
- value
- } else {
- "1px".to_string()
- }
- }
- fn height(el: &ElementNodeMut) -> String {
- if let Some(value) = el
- .get_attribute(&OwnedAttributeDiscription {
- name: "height".to_string(),
- namespace: Some("style".to_string()),
- })
- .and_then(|value| value.as_text())
- .map(|value| value.to_string())
- {
- value
- } else {
- "1px".to_string()
- }
- }
- fn update_max_width_attr(&mut self, el: &ElementNodeMut) {
- if let Some(value) = el
- .get_attribute(&OwnedAttributeDiscription {
- name: "maxlength".to_string(),
- namespace: None,
- })
- .and_then(|value| value.as_text())
- .map(|value| value.to_string())
- {
- if let Ok(max_len) = value.parse::<usize>() {
- self.max_len = Some(max_len);
- }
- }
- }
- fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
- let width = Self::width(el);
- let height = Self::height(el);
- let single_char = width
- .strip_prefix("px")
- .and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
- .is_some()
- || height
- .strip_prefix("px")
- .and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
- .is_some();
- self.border = !single_char;
- let border_style = if self.border { "solid" } else { "none" };
- el.set_attribute(
- OwnedAttributeDiscription {
- name: "border-style".to_string(),
- namespace: Some("style".to_string()),
- },
- border_style.to_string(),
- );
- }
- fn update_value_attr(&mut self, el: &ElementNodeMut) {
- if let Some(value) = el
- .get_attribute(&OwnedAttributeDiscription {
- name: "value".to_string(),
- namespace: None,
- })
- .and_then(|value| value.as_text())
- .map(|value| value.to_string())
- {
- self.text = value;
- }
- }
- pub(crate) fn set_text(&mut self, text: String, rdom: &mut RealDom, id: NodeId) {
- self.text = text;
- self.write_value(rdom, id);
- }
- pub(crate) fn text(&self) -> &str {
- self.text.as_str()
- }
- fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
- let start_highlight = self.cursor.first().idx(self.text.as_str());
- let end_highlight = self.cursor.last().idx(self.text.as_str());
- let (text_before_first_cursor, text_after_first_cursor) =
- self.text.split_at(start_highlight);
- let (text_highlighted, text_after_second_cursor) =
- text_after_first_cursor.split_at(end_highlight - start_highlight);
- if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) {
- let node_type = text.node_type_mut();
- let NodeTypeMut::Text(mut text) = node_type else {
- panic!("input must be an element")
- };
- *text.text_mut() = self.controller.display_text(text_before_first_cursor);
- }
- if let Some(mut text) = rdom.get_mut(self.highlighted_text) {
- let node_type = text.node_type_mut();
- let NodeTypeMut::Text(mut text) = node_type else {
- panic!("input must be an element")
- };
- *text.text_mut() = self.controller.display_text(text_highlighted);
- }
- if let Some(mut text) = rdom.get_mut(self.post_cursor_text) {
- let node_type = text.node_type_mut();
- let NodeTypeMut::Text(mut text) = node_type else {
- panic!("input must be an element")
- };
- *text.text_mut() = self.controller.display_text(text_after_second_cursor);
- }
- // send the event
- {
- let world = rdom.raw_world_mut();
- let data: FormData = FormData {
- value: self.text.clone(),
- values: HashMap::new(),
- files: None,
- };
- let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
- ctx.send(Event {
- id,
- name: "input",
- data: EventData::Form(data),
- bubbles: true,
- });
- }
- }
- fn handle_keydown(&mut self, mut root: NodeMut, data: &KeyboardData) {
- let key = data.key();
- let modifiers = data.modifiers();
- let code = data.code();
- if key == Key::Enter {
- return;
- }
- self.cursor.handle_input(
- &code,
- &key,
- &modifiers,
- &mut self.text,
- self.max_len.unwrap_or(1000),
- );
- let id = root.id();
- let rdom = root.real_dom_mut();
- self.write_value(rdom, id);
- let world = rdom.raw_world_mut();
- // move cursor to new position
- let taffy = {
- let query: UniqueView<Query> = world.borrow().unwrap();
- query.stretch.clone()
- };
- let taffy = taffy.lock().unwrap();
- let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
- let Point { x, y } = layout.location;
- let Pos { col, row } = self.cursor.start;
- let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
- if let Ok(pos) = crossterm::cursor::position() {
- if pos != (x, y) {
- execute!(stdout(), MoveTo(x, y)).unwrap();
- }
- } else {
- execute!(stdout(), MoveTo(x, y)).unwrap();
- }
- }
- fn handle_mousemove(&mut self, mut root: NodeMut, data: &MouseData) {
- if self.dragging {
- let id = root.id();
- let offset = data.element_coordinates();
- let mut new = Pos::new(offset.x as usize, offset.y as usize);
- // textboxs are only one line tall
- new.row = 0;
- if new != self.cursor.start {
- self.cursor.end = Some(new);
- }
- let rdom = root.real_dom_mut();
- self.write_value(rdom, id);
- }
- }
- fn handle_mousedown(&mut self, mut root: NodeMut, data: &MouseData) {
- let offset = data.element_coordinates();
- let mut new = Pos::new(offset.x as usize, offset.y as usize);
- // textboxs are only one line tall
- new.row = 0;
- new.realize_col(self.text.as_str());
- self.cursor = Cursor::from_start(new);
- self.dragging = true;
- let id = root.id();
- // move cursor to new position
- let rdom = root.real_dom_mut();
- let world = rdom.raw_world_mut();
- let taffy = {
- let query: UniqueView<Query> = world.borrow().unwrap();
- query.stretch.clone()
- };
- let taffy = taffy.lock().unwrap();
- let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
- let Point { x, y } = layout.location;
- let Pos { col, row } = self.cursor.start;
- let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
- if let Ok(pos) = crossterm::cursor::position() {
- if pos != (x, y) {
- execute!(stdout(), MoveTo(x, y)).unwrap();
- }
- } else {
- execute!(stdout(), MoveTo(x, y)).unwrap();
- }
- self.write_value(rdom, id)
- }
- }
- impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for TextLike<C> {
- const NAME: &'static str = "input";
- fn roots(&self) -> Vec<NodeId> {
- vec![self.div_wrapper]
- }
- fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
- let node_type = root.node_type();
- let NodeType::Element(el) = &*node_type else {
- panic!("input must be an element")
- };
- let value = el
- .attributes
- .get(&OwnedAttributeDiscription {
- name: "value".to_string(),
- namespace: None,
- })
- .and_then(|value| value.as_text())
- .map(|value| value.to_string());
- drop(node_type);
- let rdom = root.real_dom_mut();
- let pre_text = rdom.create_node(String::new());
- let pre_text_id = pre_text.id();
- let highlighted_text = rdom.create_node(String::new());
- let highlighted_text_id = highlighted_text.id();
- let mut highlighted_text_span = rdom.create_node(NodeType::Element(ElementNode {
- tag: "span".to_string(),
- attributes: [(
- OwnedAttributeDiscription {
- name: "background-color".to_string(),
- namespace: Some("style".to_string()),
- },
- "rgba(255, 255, 255, 50%)".to_string().into(),
- )]
- .into_iter()
- .collect(),
- ..Default::default()
- }));
- highlighted_text_span.add_child(highlighted_text_id);
- let highlighted_text_span_id = highlighted_text_span.id();
- let post_text = rdom.create_node(value.clone().unwrap_or_default());
- let post_text_id = post_text.id();
- let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
- tag: "div".to_string(),
- attributes: [(
- OwnedAttributeDiscription {
- name: "display".to_string(),
- namespace: Some("style".to_string()),
- },
- "flex".to_string().into(),
- )]
- .into_iter()
- .collect(),
- ..Default::default()
- }));
- let div_wrapper_id = div_wrapper.id();
- div_wrapper.add_child(pre_text_id);
- div_wrapper.add_child(highlighted_text_span_id);
- div_wrapper.add_child(post_text_id);
- div_wrapper.add_event_listener("mousemove");
- div_wrapper.add_event_listener("mousedown");
- div_wrapper.add_event_listener("mouseup");
- div_wrapper.add_event_listener("mouseleave");
- div_wrapper.add_event_listener("mouseenter");
- root.add_event_listener("keydown");
- root.add_event_listener("focusout");
- Self {
- pre_cursor_text: pre_text_id,
- highlighted_text: highlighted_text_id,
- post_cursor_text: post_text_id,
- div_wrapper: div_wrapper_id,
- cursor: Cursor::default(),
- text: value.unwrap_or_default(),
- ..Default::default()
- }
- }
- fn attributes_changed(
- &mut self,
- mut root: dioxus_native_core::real_dom::NodeMut,
- attributes: &dioxus_native_core::node_ref::AttributeMask,
- ) {
- match attributes {
- AttributeMask::All => {
- {
- let node_type = root.node_type_mut();
- let NodeTypeMut::Element(mut el) = node_type else {
- panic!("input must be an element")
- };
- self.update_value_attr(&el);
- self.update_size_attr(&mut el);
- self.update_max_width_attr(&el);
- }
- let id = root.id();
- self.write_value(root.real_dom_mut(), id);
- }
- AttributeMask::Some(attrs) => {
- {
- let node_type = root.node_type_mut();
- let NodeTypeMut::Element(mut el) = node_type else {
- panic!("input must be an element")
- };
- if attrs.contains("width") || attrs.contains("height") {
- self.update_size_attr(&mut el);
- }
- if attrs.contains("maxlength") {
- self.update_max_width_attr(&el);
- }
- if attrs.contains("value") {
- self.update_value_attr(&el);
- }
- }
- if attrs.contains("value") {
- let id = root.id();
- self.write_value(root.real_dom_mut(), id);
- }
- }
- }
- }
- }
- impl<C: TextLikeController + Send + Sync + Default + 'static> RinkWidget for TextLike<C> {
- fn handle_event(&mut self, event: &crate::Event, node: NodeMut) {
- match event.name {
- "keydown" => {
- if let EventData::Keyboard(data) = &event.data {
- self.handle_keydown(node, data);
- }
- }
- "mousemove" => {
- if let EventData::Mouse(data) = &event.data {
- self.handle_mousemove(node, data);
- }
- }
- "mousedown" => {
- if let EventData::Mouse(data) = &event.data {
- self.handle_mousedown(node, data);
- }
- }
- "mouseup" => {
- self.dragging = false;
- }
- "mouseleave" => {
- self.dragging = false;
- }
- "mouseenter" => {
- self.dragging = false;
- }
- "focusout" => {
- execute!(stdout(), MoveTo(0, 1000)).unwrap();
- }
- _ => {}
- }
- }
- }
|