make TUI widgets cross framework

This commit is contained in:
Evan Almloff committed 2023-03-24 12:43:51 -05:00
1 parent 9a16687bdc
commit 625dc662d2
32 files changed
+3023 -1091

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
use std::collections::HashMap;
use dioxus_html::input_data::keyboard_types::Key;
use dioxus_native_core::{
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::NodeType,
real_dom::{ElementNodeMut, NodeImmutable, NodeTypeMut, RealDom},
utils::widget_watcher::Widget,
NodeId,
};
use shipyard::UniqueViewMut;
use crate::FormData;
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct Button {
div_id: NodeId,
text_id: NodeId,
value: String,
}
impl Button {
fn width(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "width".to_string(),
namespace: None,
})
.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: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
let width = Self::width(el);
let height = Self::height(el);
let single_char = width == "1px" || height == "1px";
let border_style = if single_char { "none" } else { "solid" };
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.value = value;
}
}
fn write_value(&self, rdom: &mut RealDom) {
if let Some(mut text) = rdom.get_mut(self.text_id) {
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.value.clone();
}
}
fn switch(&mut self, ctx: &mut WidgetContext) {
let data = FormData {
value: self.value.to_string(),
values: HashMap::new(),
files: None,
};
ctx.send(crate::Event {
id: self.div_id,
name: "input",
data: crate::EventData::Form(data),
bubbles: true,
});
}
}
impl Widget for Button {
const NAME: &'static str = "input";
fn create(root: &mut 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 text = rdom.create_node(value.clone().unwrap_or_default());
let text_id = text.id();
root.add_event_listener("keydown");
root.add_event_listener("click");
let div_id = root.id();
root.add_child(text_id);
Self {
div_id,
text_id,
value: value.unwrap_or_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.write_value(root.real_dom_mut());
}
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("value") {
self.update_value_attr(&el);
}
}
if attrs.contains("value") {
self.write_value(root.real_dom_mut());
}
}
}
}
}
impl RinkWidget for Button {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::NodeMut,
) {
let mut ctx: UniqueViewMut<WidgetContext> = node
.real_dom_mut()
.raw_world_mut()
.borrow()
.expect("expected widget context");
match event.name {
"click" => self.switch(&mut ctx),
"keydown" => {
if let crate::EventData::Keyboard(data) = &event.data {
if !data.is_auto_repeating()
&& match data.key() {
Key::Character(c) if c == " " => true,
Key::Enter => true,
_ => false,
}
{
self.switch(&mut ctx);
}
}
}
_ => {}
}
}
}
+250
View File
@@ -0,0 +1,250 @@
use std::collections::HashMap;
use dioxus_html::input_data::keyboard_types::Key;
use dioxus_native_core::{
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::NodeType,
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut},
utils::widget_watcher::Widget,
NodeId,
};
use shipyard::UniqueView;
use crate::FormData;
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct CheckBox {
div_id: NodeId,
text_id: NodeId,
value: String,
checked: bool,
}
impl CheckBox {
fn width(el: &ElementNodeMut) -> String {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "width".to_string(),
namespace: None,
})
.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: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
value
} else {
"1px".to_string()
}
}
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
let width = Self::width(el);
let height = Self::height(el);
let single_char = width == "1px" || height == "1px";
let border_style = if single_char { "none" } else { "solid" };
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) {
self.value = el
.get_attribute(&OwnedAttributeDiscription {
name: "value".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
.unwrap_or_else(|| "on".to_string());
}
fn update_checked_attr(&mut self, el: &ElementNodeMut) {
self.checked = el
.get_attribute(&OwnedAttributeDiscription {
name: "checked".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
.unwrap_or_else(|| "false".to_string())
== "true";
}
fn write_value(&self, root: &mut NodeMut) {
let single_char = {
let node_type = root.node_type_mut();
let NodeTypeMut::Element( el) = node_type else { panic!("input must be an element") };
Self::width(&el) == "1px" || Self::height(&el) == "1px"
};
let rdom = root.real_dom_mut();
if let Some(mut text) = rdom.get_mut(self.text_id) {
let node_type = text.node_type_mut();
let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") };
let value = if single_char {
if self.checked {
"☑"
} else {
"☐"
}
} else if self.checked {
"✓"
} else {
" "
};
*text.text_mut() = value.to_string();
}
}
fn switch(&mut self, node: &mut NodeMut) {
let new_state = !self.checked;
let data = FormData {
value: new_state
.then(|| self.value.to_string())
.unwrap_or_default(),
values: HashMap::new(),
files: None,
};
{
let ctx: UniqueView<WidgetContext> = node
.real_dom_mut()
.raw_world_mut()
.borrow()
.expect("expected widget context");
ctx.send(crate::Event {
id: self.div_id,
name: "input",
data: crate::EventData::Form(data),
bubbles: true,
});
}
self.checked = new_state;
self.write_value(node);
}
}
impl Widget for CheckBox {
const NAME: &'static str = "input";
fn create(root: &mut 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 text = rdom.create_node(String::new());
let text_id = text.id();
root.add_event_listener("click");
root.add_event_listener("keydown");
let div_id = root.id();
root.add_child(text_id);
let myself = Self {
div_id,
text_id,
value: value.unwrap_or_default(),
checked: false,
};
myself.write_value(root);
myself
}
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_checked_attr(&el);
}
self.write_value(&mut root);
}
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("value") {
self.update_value_attr(&el);
}
if attrs.contains("checked") {
self.update_checked_attr(&el);
}
}
if attrs.contains("checked") {
self.write_value(&mut root);
}
}
}
}
}
impl RinkWidget for CheckBox {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::NodeMut,
) {
match event.name {
"click" => self.switch(node),
"keydown" => {
if let crate::EventData::Keyboard(data) = &event.data {
if !data.is_auto_repeating()
&& match data.key() {
Key::Character(c) if c == " " => true,
Key::Enter => true,
_ => false,
}
{
self.switch(node);
}
}
}
_ => {}
}
}
}
+132
View File
@@ -0,0 +1,132 @@
use dioxus_native_core::{
node::OwnedAttributeDiscription, prelude::NodeType, real_dom::NodeImmutable,
utils::widget_watcher::Widget,
};
use super::{
checkbox::CheckBox, number::Number, password::Password, slider::Slider, textbox::TextBox,
RinkWidget,
};
use crate::widgets::button::Button;
pub(crate) enum Input {
Button(Button),
CheckBox(CheckBox),
TextBox(TextBox),
Password(Password),
Number(Number),
Slider(Slider),
}
impl Widget for Input {
const NAME: &'static str = "input";
fn create(root: &mut dioxus_native_core::real_dom::NodeMut<()>) -> Self {
{
// currently widgets are not allowed to have children
let children = root.child_ids();
let rdom = root.real_dom_mut();
for child in children {
if let Some(mut child) = rdom.get_mut(child) {
child.remove();
}
}
}
let node_type = root.node_type();
let NodeType::Element(el) = &*node_type else { panic!("input must be an element") };
let input_type = el
.attributes
.get(&OwnedAttributeDiscription {
name: "type".to_string(),
namespace: None,
})
.and_then(|value| value.as_text());
match input_type {
Some("button") => {
drop(node_type);
Input::Button(Button::create(root))
}
Some("checkbox") => {
drop(node_type);
Input::CheckBox(CheckBox::create(root))
}
Some("textbox") => {
drop(node_type);
Input::TextBox(TextBox::create(root))
}
Some("password") => {
drop(node_type);
Input::Password(Password::create(root))
}
Some("number") => {
drop(node_type);
Input::Number(Number::create(root))
}
Some("range") => {
drop(node_type);
Input::Slider(Slider::create(root))
}
_ => {
drop(node_type);
Input::TextBox(TextBox::create(root))
}
}
}
fn attributes_changed(
&mut self,
root: dioxus_native_core::real_dom::NodeMut<()>,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
match self {
Input::Button(button) => {
button.attributes_changed(root, attributes);
}
Input::CheckBox(checkbox) => {
checkbox.attributes_changed(root, attributes);
}
Input::TextBox(textbox) => {
textbox.attributes_changed(root, attributes);
}
Input::Password(password) => {
password.attributes_changed(root, attributes);
}
Input::Number(number) => {
number.attributes_changed(root, attributes);
}
Input::Slider(slider) => {
slider.attributes_changed(root, attributes);
}
}
}
}
impl RinkWidget for Input {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::NodeMut,
) {
match self {
Input::Button(button) => {
button.handle_event(event, node);
}
Input::CheckBox(checkbox) => {
checkbox.handle_event(event, node);
}
Input::TextBox(textbox) => {
textbox.handle_event(event, node);
}
Input::Password(password) => {
password.handle_event(event, node);
}
Input::Number(number) => {
number.handle_event(event, node);
}
Input::Slider(slider) => {
slider.handle_event(event, node);
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
mod button;
mod checkbox;
mod input;
mod number;
mod password;
mod slider;
mod textbox;
use std::sync::{Arc, RwLock};
use dioxus_native_core::{
real_dom::RealDom,
utils::widget_watcher::{Widget, WidgetFactory, WidgetUpdater, WidgetWatcher},
};
use futures_channel::mpsc::UnboundedSender;
use shipyard::{Component, Unique};
use crate::Event;
pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender<Event>) {
// inject the widget context
rdom.raw_world().add_unique(WidgetContext { sender });
// create the widget watcher
let mut widget_watcher = WidgetWatcher::default();
widget_watcher
.register_widget::<RinkWidgetTraitObjectFactory<input::Input>, RinkWidgetTraitObject>();
widget_watcher.attach(rdom);
}
trait RinkWidget: Sync + Send + Widget + 'static {
fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut);
}
pub trait RinkWidgetResponder: WidgetUpdater {
fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut);
}
impl<W: RinkWidget> RinkWidgetResponder for W {
fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) {
RinkWidget::handle_event(self, event, node)
}
}
struct RinkWidgetTraitObjectFactory<W: RinkWidget> {
_marker: std::marker::PhantomData<W>,
}
impl<W: RinkWidget> WidgetFactory<RinkWidgetTraitObject> for RinkWidgetTraitObjectFactory<W> {
const NAME: &'static str = W::NAME;
fn create(node: &mut dioxus_native_core::real_dom::NodeMut) -> RinkWidgetTraitObject {
let myself = RinkWidgetTraitObject {
widget: Arc::new(RwLock::new(W::create(node))),
};
node.insert(myself.clone());
myself
}
}
#[derive(Clone, Component)]
pub(crate) struct RinkWidgetTraitObject {
widget: Arc<RwLock<dyn RinkWidgetResponder + Send + Sync>>,
}
impl WidgetUpdater for RinkWidgetTraitObject {
fn attributes_changed(
&mut self,
root: dioxus_native_core::real_dom::NodeMut,
attributes: &dioxus_native_core::node_ref::AttributeMask,
) {
let mut widget = self.widget.write().unwrap();
widget.attributes_changed(root, attributes);
}
}
impl RinkWidgetResponder for RinkWidgetTraitObject {
fn handle_event(&mut self, event: &Event, node: &mut dioxus_native_core::real_dom::NodeMut) {
let mut widget = self.widget.write().unwrap();
widget.handle_event(event, node);
}
}
#[derive(Unique)]
pub(crate) struct WidgetContext {
sender: UnboundedSender<Event>,
}
impl WidgetContext {
pub(crate) fn send(&self, event: Event) {
self.sender.unbounded_send(event).unwrap();
}
}
+464
View File
@@ -0,0 +1,464 @@
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::{
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::{ElementNode, NodeType},
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
utils::{
cursor::{Cursor, Pos},
widget_watcher::Widget,
},
NodeId,
};
use shipyard::UniqueView;
use taffy::geometry::Point;
use crate::{query::get_layout, Event, EventData, FormData, Query};
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct Number {
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>,
}
impl Number {
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;
}
}
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() = text_before_first_cursor.to_string();
}
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() = text_highlighted.to_string();
}
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() = text_after_second_cursor.to_string();
}
// send the event
{
let world = rdom.raw_world_mut();
let data = 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 increase(&mut self) {
let num = self.text.parse::<f64>().unwrap_or(0.0);
self.text = (num + 1.0).to_string();
}
fn decrease(&mut self) {
let num = self.text.parse::<f64>().unwrap_or(0.0);
self.text = (num - 1.0).to_string();
}
fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) {
let key = data.key();
let is_text = match key.clone() {
Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
Key::Character(c) if c == "." || c == "-" || c.chars().all(|c| c.is_numeric()) => true,
_ => false,
};
if is_text {
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();
}
} else {
match key {
Key::ArrowUp => {
self.increase();
}
Key::ArrowDown => {
self.decrease();
}
_ => (),
}
let id = root.id();
let rdom = root.real_dom_mut();
self.write_value(rdom, id);
}
}
fn handle_mousemove(&mut self, root: &mut 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);
if self.border {
new.col = new.col.saturating_sub(1);
}
// 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, root: &mut NodeMut, data: &MouseData) {
let offset = data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if self.border {
new.col = new.col.saturating_sub(1);
}
// 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 Widget for Number {
const NAME: &'static str = "input";
fn create(root: &mut 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");
root.add_child(div_wrapper_id);
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 RinkWidget for Number {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::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();
}
_ => {}
}
}
}
+432
View File
@@ -0,0 +1,432 @@
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::{
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::{ElementNode, NodeType},
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
utils::{
cursor::{Cursor, Pos},
widget_watcher::Widget,
},
NodeId,
};
use shipyard::UniqueView;
use taffy::geometry::Point;
use crate::{query::get_layout, Event, EventData, FormData, Query};
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct Password {
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>,
}
impl Password {
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;
}
}
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() = ".".repeat(text_before_first_cursor.len());
}
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() = ".".repeat(text_highlighted.len());
}
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() = ".".repeat(text_after_second_cursor.len());
}
// send the event
{
let world = rdom.raw_world_mut();
let data = 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, root: &mut 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, root: &mut 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);
if self.border {
new.col = new.col.saturating_sub(1);
}
// 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, root: &mut NodeMut, data: &MouseData) {
let offset = data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if self.border {
new.col = new.col.saturating_sub(1);
}
// 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 Widget for Password {
const NAME: &'static str = "input";
fn create(root: &mut 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");
root.add_child(div_wrapper_id);
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 RinkWidget for Password {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::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();
}
_ => {}
}
}
}
+458
View File
@@ -0,0 +1,458 @@
use std::collections::HashMap;
use dioxus_html::{input_data::keyboard_types::Key, KeyboardData, MouseData};
use dioxus_native_core::{
node::{OwnedAttributeDiscription, OwnedAttributeValue},
node_ref::AttributeMask,
prelude::{ElementNode, NodeType},
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
utils::widget_watcher::Widget,
NodeId,
};
use shipyard::UniqueView;
use super::{RinkWidget, WidgetContext};
use crate::{query::get_layout, Event, EventData, FormData, Query};
#[derive(Debug)]
pub(crate) struct Slider {
div_wrapper: NodeId,
pre_cursor_div: NodeId,
post_cursor_div: NodeId,
min: f64,
max: f64,
step: Option<f64>,
value: f64,
border: bool,
}
impl Default for Slider {
fn default() -> Self {
Self {
div_wrapper: Default::default(),
pre_cursor_div: Default::default(),
post_cursor_div: Default::default(),
min: 0.0,
max: 100.0,
step: None,
value: 0.0,
border: false,
}
}
}
impl Slider {
fn size(&self) -> f64 {
self.max - self.min
}
fn step(&self) -> f64 {
self.step.unwrap_or(self.size() / 10.0)
}
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_min_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "min".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.min = value.parse().ok().unwrap_or(0.0);
}
}
fn update_max_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "max".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.max = value.parse().ok().unwrap_or(100.0);
}
}
fn update_step_attr(&mut self, el: &ElementNodeMut) {
if let Some(value) = el
.get_attribute(&OwnedAttributeDiscription {
name: "step".to_string(),
namespace: None,
})
.and_then(|value| value.as_text())
.map(|value| value.to_string())
{
self.step = value.parse().ok();
}
}
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(&mut self, new: f64) {
self.value = new.clamp(self.min, self.max);
}
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.update_value(value.parse().ok().unwrap_or(0.0));
}
}
fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
let value_percent = (self.value - self.min) / self.size() * 100.0;
if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) {
let node_type = div.node_type_mut();
let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") };
element.set_attribute(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
format!("{}%", value_percent),
);
}
if let Some(mut div) = rdom.get_mut(self.post_cursor_div) {
let node_type = div.node_type_mut();
let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") };
element.set_attribute(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
format!("{}%", 100.0 - value_percent),
);
}
// send the event
let world = rdom.raw_world_mut();
{
let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
let data = FormData {
value: self.value.to_string(),
values: HashMap::new(),
files: None,
};
ctx.send(Event {
id,
name: "input",
data: EventData::Form(data),
bubbles: true,
});
}
}
fn handle_keydown(&mut self, root: &mut NodeMut, data: &KeyboardData) {
let key = data.key();
let step = self.step();
match key {
Key::ArrowDown | Key::ArrowLeft => {
self.update_value(self.value - step);
}
Key::ArrowUp | Key::ArrowRight => {
self.update_value(self.value + step);
}
_ => {
return;
}
}
let id = root.id();
let rdom = root.real_dom_mut();
self.write_value(rdom, id);
}
fn handle_mousemove(&mut self, root: &mut NodeMut, data: &MouseData) {
if !data.held_buttons().is_empty() {
let id = root.id();
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 width = layout.size.width as f64;
let offset = data.element_coordinates();
self.update_value(self.min + self.size() * offset.x / width);
self.write_value(rdom, id);
}
}
}
impl Widget for Slider {
const NAME: &'static str = "input";
fn create(root: &mut 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,
});
let value = value
.and_then(|value| match value {
OwnedAttributeValue::Text(text) => text.as_str().parse().ok(),
OwnedAttributeValue::Float(float) => Some(*float),
OwnedAttributeValue::Int(int) => Some(*int as f64),
_ => None,
})
.unwrap_or(0.0);
drop(node_type);
let rdom = root.real_dom_mut();
let pre_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [(
OwnedAttributeDiscription {
name: "background-color".to_string(),
namespace: Some("style".to_string()),
},
"rgba(10,10,10,0.5)".to_string().into(),
)]
.into_iter()
.collect(),
..Default::default()
}));
let pre_cursor_div_id = pre_cursor_div.id();
let cursor_text = rdom.create_node("|".to_string());
let cursor_text_id = cursor_text.id();
let mut cursor_span = rdom.create_node(NodeType::Element(ElementNode {
tag: "div".to_string(),
attributes: [].into_iter().collect(),
..Default::default()
}));
cursor_span.add_child(cursor_text_id);
let cursor_span_id = cursor_span.id();
let post_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
tag: "span".to_string(),
attributes: [
(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
"100%".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "background-color".to_string(),
namespace: Some("style".to_string()),
},
"rgba(10,10,10,0.5)".to_string().into(),
),
]
.into_iter()
.collect(),
..Default::default()
}));
let post_cursor_div_id = post_cursor_div.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(),
),
(
OwnedAttributeDiscription {
name: "flex-direction".to_string(),
namespace: Some("style".to_string()),
},
"row".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "width".to_string(),
namespace: Some("style".to_string()),
},
"100%".to_string().into(),
),
(
OwnedAttributeDiscription {
name: "height".to_string(),
namespace: Some("style".to_string()),
},
"100%".to_string().into(),
),
]
.into_iter()
.collect(),
..Default::default()
}));
let div_wrapper_id = div_wrapper.id();
div_wrapper.add_child(pre_cursor_div_id);
div_wrapper.add_child(cursor_span_id);
div_wrapper.add_child(post_cursor_div_id);
div_wrapper.add_event_listener("mousemove");
div_wrapper.add_event_listener("mousedown");
root.add_event_listener("keydown");
root.add_child(div_wrapper_id);
Self {
pre_cursor_div: pre_cursor_div_id,
post_cursor_div: post_cursor_div_id,
div_wrapper: div_wrapper_id,
value,
..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_attr(&el);
self.update_min_attr(&el);
self.update_step_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("max") {
self.update_max_attr(&el);
}
if attrs.contains("min") {
self.update_min_attr(&el);
}
if attrs.contains("step") {
self.update_step_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 RinkWidget for Slider {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::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_mousemove(node, data);
}
}
_ => {}
}
}
}
+432
View File
@@ -0,0 +1,432 @@
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::{
node::OwnedAttributeDiscription,
node_ref::AttributeMask,
prelude::{ElementNode, NodeType},
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
utils::{
cursor::{Cursor, Pos},
widget_watcher::Widget,
},
NodeId,
};
use shipyard::UniqueView;
use taffy::geometry::Point;
use crate::{query::get_layout, Event, EventData, FormData, Query};
use super::{RinkWidget, WidgetContext};
#[derive(Debug, Default)]
pub(crate) struct TextBox {
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>,
}
impl TextBox {
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;
}
}
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() = text_before_first_cursor.to_string();
}
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() = text_highlighted.to_string();
}
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() = text_after_second_cursor.to_string();
}
// send the event
{
let world = rdom.raw_world_mut();
let data = 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, root: &mut 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, root: &mut 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);
if self.border {
new.col = new.col.saturating_sub(1);
}
// 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, root: &mut NodeMut, data: &MouseData) {
let offset = data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if self.border {
new.col = new.col.saturating_sub(1);
}
// 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 Widget for TextBox {
const NAME: &'static str = "input";
fn create(root: &mut 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");
root.add_child(div_wrapper_id);
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 RinkWidget for TextBox {
fn handle_event(
&mut self,
event: &crate::Event,
node: &mut dioxus_native_core::real_dom::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();
}
_ => {}
}
}
}