Move dioxus-specifc code into dioxus-tui crate and rename core crate to Rink

This commit is contained in:
Evan Almloff committed 2023-03-13 12:04:51 -05:00
1 parent 8e8c89c99e
commit f34fa5d974
55 files changed
+241 -55

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
pub mod prelude;
pub mod widgets;
use std::{
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
};
use dioxus_core::{Component, ElementId, VirtualDom};
use dioxus_html::EventData;
use dioxus_native_core::{
dioxus::{DioxusState, NodeImmutableDioxusExt},
NodeId, RealDom,
};
pub use rink::Config;
use rink::{query::Query, render, Renderer, TuiContext};
pub fn launch(app: Component<()>) {
launch_cfg(app, Config::default())
}
pub fn launch_cfg(app: Component<()>, cfg: Config) {
launch_cfg_with_props(app, (), cfg);
}
pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props, cfg: Config) {
render(cfg, |rdom, taffy, event_tx| {
let dioxus_state = {
let mut rdom = rdom.write().unwrap();
DioxusState::create(&mut rdom)
};
let dioxus_state = Rc::new(RwLock::new(dioxus_state));
let mut vdom = VirtualDom::new_with_props(app, props)
.with_root_context(TuiContext::new(event_tx))
.with_root_context(Query::new(rdom.clone(), taffy.clone()))
.with_root_context(DioxusElementToNodeId {
mapping: dioxus_state.clone(),
});
let muts = vdom.rebuild();
let mut rdom = rdom.write().unwrap();
dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
DioxusRenderer {
vdom,
dioxus_state,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: {
let (hot_reload_tx, hot_reload_rx) =
tokio::sync::mpsc::unbounded_channel::<dioxus_hot_reload::HotReloadMsg>();
dioxus_hot_reload::connect(move |msg| {
let _ = hot_reload_tx.send(msg);
});
hot_reload_rx
},
}
})
.unwrap();
}
struct DioxusRenderer {
vdom: VirtualDom,
dioxus_state: Rc<RwLock<DioxusState>>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
impl Renderer for DioxusRenderer {
fn render(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let muts = self.vdom.render_immediate();
{
let mut rdom = rdom.write().unwrap();
self.dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
}
}
fn handle_event(
&mut self,
rdom: &Arc<RwLock<RealDom>>,
id: NodeId,
event: &str,
value: Rc<EventData>,
bubbles: bool,
) {
let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
if let Some(id) = id {
self.vdom
.handle_event(event, value.deref().clone().into_any(), id, bubbles);
}
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
#[cfg(all(feature = "hot-reload", debug_assertions))]
return Box::pin(async {
let hot_reload_wait = self.hot_reload_rx.recv();
let mut hot_reload_msg = None;
let wait_for_work = self.vdom.wait_for_work();
tokio::select! {
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
{
hot_reload_msg = Some(msg);
}
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
let () = msg;
}
_ = wait_for_work => {}
}
// if we have a new template, replace the old one
if let Some(msg) = hot_reload_msg {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
self.vdom.replace_template(template);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
}
}
});
#[cfg(not(all(feature = "hot-reload", debug_assertions)))]
Box::pin(self.vdom.wait_for_work())
}
}
#[derive(Clone)]
pub struct DioxusElementToNodeId {
mapping: Rc<RwLock<DioxusState>>,
}
impl DioxusElementToNodeId {
pub fn get_node_id(&self, element_id: ElementId) -> Option<NodeId> {
self.mapping
.read()
.unwrap()
.try_element_to_node_id(element_id)
}
}
+2
View File
@@ -0,0 +1,2 @@
pub use crate::widgets::*;
pub use rink::Config;
+59
View File
@@ -0,0 +1,59 @@
use std::collections::HashMap;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
#[derive(Props)]
pub(crate) struct ButtonProps<'a> {
#[props(!optional)]
raw_onclick: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
let state = use_state(cx, || false);
let width = cx.props.width.unwrap_or("1px");
let height = cx.props.height.unwrap_or("1px");
let single_char = width == "1px" || height == "1px";
let text = if let Some(v) = cx.props.value { v } else { "" };
let border_style = if single_char { "none" } else { "solid" };
let update = || {
let new_state = !state.get();
if let Some(callback) = cx.props.raw_onclick {
callback.call(FormData {
value: text.to_string(),
values: HashMap::new(),
files: None,
});
}
state.set(new_state);
};
render! {
div{
width: "{width}",
height: "{height}",
border_style: "{border_style}",
flex_direction: "row",
align_items: "center",
justify_content: "center",
onclick: move |_| {
update();
},
onkeydown: move |evt|{
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
update();
}
},
"{text}"
}
}
}
@@ -0,0 +1,82 @@
use std::collections::HashMap;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
#[derive(Props)]
pub(crate) struct CheckBoxProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
#[props(!optional)]
checked: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some());
let width = cx.props.width.unwrap_or("1px");
let height = cx.props.height.unwrap_or("1px");
let single_char = width == "1px" && height == "1px";
let text = if single_char {
if *state.get() {
"☑"
} else {
"☐"
}
} else if *state.get() {
"✓"
} else {
" "
};
let border_style = if width == "1px" || height == "1px" {
"none"
} else {
"solid"
};
let update = move || {
let new_state = !state.get();
if let Some(callback) = cx.props.raw_oninput {
callback.call(FormData {
value: if let Some(value) = &cx.props.value {
if new_state {
value.to_string()
} else {
String::new()
}
} else {
"on".to_string()
},
values: HashMap::new(),
files: None,
});
}
state.set(new_state);
};
render! {
div {
width: "{width}",
height: "{height}",
border_style: "{border_style}",
align_items: "center",
justify_content: "center",
onclick: move |_| {
update();
},
onkeydown: move |evt| {
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
update();
}
},
"{text}"
}
}
}
+102
View File
@@ -0,0 +1,102 @@
use dioxus::prelude::*;
use dioxus_core::prelude::fc_to_builder;
use dioxus_html::FormData;
use crate::widgets::button::Button;
use crate::widgets::checkbox::CheckBox;
use crate::widgets::number::NumbericInput;
use crate::widgets::password::Password;
use crate::widgets::slider::Slider;
use crate::widgets::textbox::TextBox;
#[derive(Props)]
pub struct InputProps<'a> {
r#type: Option<&'static str>,
oninput: Option<EventHandler<'a, FormData>>,
onclick: Option<EventHandler<'a, FormData>>,
value: Option<&'a str>,
size: Option<&'a str>,
maxlength: Option<&'a str>,
width: Option<&'a str>,
height: Option<&'a str>,
min: Option<&'a str>,
max: Option<&'a str>,
step: Option<&'a str>,
checked: Option<&'a str>,
}
#[allow(non_snake_case)]
pub fn Input<'a>(cx: Scope<'a, InputProps<'a>>) -> Element<'a> {
cx.render(match cx.props.r#type {
Some("checkbox") => {
rsx! {
CheckBox{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
width: cx.props.width,
height: cx.props.height,
checked: cx.props.checked,
}
}
}
Some("range") => {
rsx! {
Slider{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
width: cx.props.width,
height: cx.props.height,
max: cx.props.max,
min: cx.props.min,
step: cx.props.step,
}
}
}
Some("button") => {
rsx! {
Button{
raw_onclick: cx.props.onclick.as_ref(),
value: cx.props.value,
width: cx.props.width,
height: cx.props.height,
}
}
}
Some("number") => {
rsx! {
NumbericInput{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
size: cx.props.size,
max_length: cx.props.maxlength,
width: cx.props.width,
height: cx.props.height,
}
}
}
Some("password") => {
rsx! {
Password{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
size: cx.props.size,
max_length: cx.props.maxlength,
width: cx.props.width,
height: cx.props.height,
}
}
}
_ => {
rsx! {
TextBox{
raw_oninput: cx.props.oninput.as_ref(),
value: cx.props.value,
size: cx.props.size,
max_length: cx.props.maxlength,
width: cx.props.width,
height: cx.props.height,
}
}
}
})
}
+22
View File
@@ -0,0 +1,22 @@
mod button;
mod checkbox;
mod input;
mod number;
mod password;
mod slider;
mod textbox;
use dioxus_core::{RenderReturn, Scope};
use dioxus_native_core::NodeId;
pub use input::*;
use crate::DioxusElementToNodeId;
pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<NodeId> {
if let RenderReturn::Ready(sync) = cx.root_node() {
let mapping: DioxusElementToNodeId = cx.consume_context()?;
mapping.get_node_id(sync.root_ids.get(0)?)
} else {
None
}
}
+209
View File
@@ -0,0 +1,209 @@
use crate::widgets::get_root_id;
use crossterm::{cursor::MoveTo, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use rink::Query;
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
#[derive(Props)]
pub(crate) struct NumbericInputProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
size: Option<&'a str>,
#[props(!optional)]
max_length: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let tui_query_clone = tui_query.clone();
let text_ref = use_ref(cx, || {
if let Some(intial_text) = cx.props.value {
intial_text.to_string()
} else {
String::new()
}
});
let cursor = use_ref(cx, Cursor::default);
let dragging = use_state(cx, || false);
let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&*text);
let end_highlight = cursor.read().last().idx(&*text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
let max_len = cx
.props
.max_length
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let width = cx
.props
.width
.map(|s| s.to_string())
// px is the same as em in tui
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
.unwrap_or_else(|| "10px".to_string());
let height = cx.props.height.unwrap_or("3px");
// don't draw a border unless there is enough space
let border = if width
.strip_suffix("px")
.and_then(|w| w.parse::<i32>().ok())
.filter(|w| *w < 3)
.is_some()
|| height
.strip_suffix("px")
.and_then(|h| h.parse::<i32>().ok())
.filter(|h| *h < 3)
.is_some()
{
"none"
} else {
"solid"
};
let update = |text: String| {
if let Some(input_handler) = &cx.props.raw_oninput {
input_handler.call(FormData {
value: text,
values: HashMap::new(),
files: None,
});
}
};
let increase = move || {
let mut text = text_ref.write();
*text = (text.parse::<f64>().unwrap_or(0.0) + 1.0).to_string();
update(text.clone());
};
let decrease = move || {
let mut text = text_ref.write();
*text = (text.parse::<f64>().unwrap_or(0.0) - 1.0).to_string();
update(text.clone());
};
render! {
div{
width: "{width}",
height: "{height}",
border_style: "{border}",
onkeydown: move |k| {
let is_text = match k.key(){
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 mut text = text_ref.write();
cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
update(text.clone());
let node = tui_query.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
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 k.key() {
Key::ArrowUp =>{
increase();
}
Key::ArrowDown =>{
decrease();
}
_ => ()
}
}
},
onmousemove: move |evt| {
if *dragging.get() {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
if new != cursor.read().start {
cursor.write().end = Some(new);
}
}
},
onmousedown: move |evt| {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
new.row = 0;
new.realize_col(text_ref.read().as_str());
cursor.set(Cursor::from_start(new));
dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmouseup: move |_| {
dragging.set(false);
},
onmouseleave: move |_| {
dragging.set(false);
},
onmouseenter: move |_| {
dragging.set(false);
},
onfocusout: |_| {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
},
"{text_before_first_cursor}"
span{
background_color: "rgba(255, 255, 255, 50%)",
"{text_highlighted}"
}
"{text_after_second_cursor}"
}
}
}
+193
View File
@@ -0,0 +1,193 @@
use crate::widgets::get_root_id;
use crossterm::{cursor::*, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use rink::Query;
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
#[derive(Props)]
pub(crate) struct PasswordProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
size: Option<&'a str>,
#[props(!optional)]
max_length: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let tui_query_clone = tui_query.clone();
let text_ref = use_ref(cx, || {
if let Some(intial_text) = cx.props.value {
intial_text.to_string()
} else {
String::new()
}
});
let cursor = use_ref(cx, Cursor::default);
let dragging = use_state(cx, || false);
let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&*text);
let end_highlight = cursor.read().last().idx(&*text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
let text_before_first_cursor = ".".repeat(text_before_first_cursor.len());
let text_highlighted = ".".repeat(text_highlighted.len());
let text_after_second_cursor = ".".repeat(text_after_second_cursor.len());
let max_len = cx
.props
.max_length
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let width = cx
.props
.width
.map(|s| s.to_string())
// px is the same as em in tui
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
.unwrap_or_else(|| "10px".to_string());
let height = cx.props.height.unwrap_or("3px");
// don't draw a border unless there is enough space
let border = if width
.strip_suffix("px")
.and_then(|w| w.parse::<i32>().ok())
.filter(|w| *w < 3)
.is_some()
|| height
.strip_suffix("px")
.and_then(|h| h.parse::<i32>().ok())
.filter(|h| *h < 3)
.is_some()
{
"none"
} else {
"solid"
};
let onkeydown = move |k: KeyboardEvent| {
if k.key() == Key::Enter {
return;
}
let mut text = text_ref.write();
cursor
.write()
.handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput {
input_handler.call(FormData {
value: text.clone(),
values: HashMap::new(),
files: None,
});
}
let node = tui_query.get(get_root_id(cx).unwrap());
let Point { x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (
col as u16 + x as u16 + u16::from(border != "none"),
row as u16 + y as u16 + u16::from(border != "none"),
);
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y) {
execute!(stdout(), MoveTo(x, y)).unwrap();
}
} else {
execute!(stdout(), MoveTo(x, y)).unwrap();
}
};
render! {
div {
width: "{width}",
height: "{height}",
border_style: "{border}",
onkeydown: onkeydown,
onmousemove: move |evt| {
if *dragging.get() {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
if new != cursor.read().start {
cursor.write().end = Some(new);
}
}
},
onmousedown: move |evt| {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
new.realize_col(text_ref.read().as_str());
cursor.set(Cursor::from_start(new));
dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmouseup: move |_| {
dragging.set(false);
},
onmouseleave: move |_| {
dragging.set(false);
},
onmouseenter: move |_| {
dragging.set(false);
},
onfocusout: |_| {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
},
"{text_before_first_cursor}"
span{
background_color: "rgba(255, 255, 255, 50%)",
"{text_highlighted}"
}
"{text_after_second_cursor}"
}
}
}
+107
View File
@@ -0,0 +1,107 @@
use std::collections::HashMap;
use crate::widgets::get_root_id;
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use rink::Query;
#[derive(Props)]
pub(crate) struct SliderProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
#[props(!optional)]
min: Option<&'a str>,
#[props(!optional)]
max: Option<&'a str>,
#[props(!optional)]
step: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let value_state = use_state(cx, || 0.0);
let value: Option<f32> = cx.props.value.and_then(|v| v.parse().ok());
let width = cx.props.width.unwrap_or("20px");
let height = cx.props.height.unwrap_or("1px");
let min = cx.props.min.and_then(|v| v.parse().ok()).unwrap_or(0.0);
let max = cx.props.max.and_then(|v| v.parse().ok()).unwrap_or(100.0);
let size = max - min;
let step = cx
.props
.step
.and_then(|v| v.parse().ok())
.unwrap_or(size / 10.0);
let current_value = match value {
Some(value) => value,
None => *value_state.get(),
}
.clamp(min, max);
let fst_width = 100.0 * (current_value - min) / size;
let snd_width = 100.0 * (max - current_value) / size;
assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0);
let update = |value: String| {
if let Some(oninput) = cx.props.raw_oninput {
oninput.call(FormData {
value,
values: HashMap::new(),
files: None,
});
}
};
render! {
div{
width: "{width}",
height: "{height}",
display: "flex",
flex_direction: "row",
onkeydown: move |event| {
match event.key() {
Key::ArrowLeft => {
value_state.set((current_value - step).clamp(min, max));
update(value_state.current().to_string());
}
Key::ArrowRight => {
value_state.set((current_value + step).clamp(min, max));
update(value_state.current().to_string());
}
_ => ()
}
},
onmousemove: move |evt| {
let mouse = evt.data;
if !mouse.held_buttons().is_empty(){
let node = tui_query.get(get_root_id(cx).unwrap());
let width = node.size().unwrap().width;
let offset = mouse.element_coordinates();
value_state.set(min + size*(offset.x as f32) / width as f32);
update(value_state.current().to_string());
}
},
div{
width: "{fst_width}%",
background_color: "rgba(10,10,10,0.5)",
}
div{
"|"
}
div{
width: "{snd_width}%",
background_color: "rgba(10,10,10,0.5)",
}
}
}
}
+182
View File
@@ -0,0 +1,182 @@
use crate::widgets::get_root_id;
use crossterm::{cursor::*, execute};
use dioxus::prelude::*;
use dioxus_elements::input_data::keyboard_types::Key;
use dioxus_html as dioxus_elements;
use dioxus_html::FormData;
use dioxus_native_core::utils::cursor::{Cursor, Pos};
use rink::Query;
use std::{collections::HashMap, io::stdout};
use taffy::geometry::Point;
#[derive(Props)]
pub(crate) struct TextBoxProps<'a> {
#[props(!optional)]
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
#[props(!optional)]
value: Option<&'a str>,
#[props(!optional)]
size: Option<&'a str>,
#[props(!optional)]
max_length: Option<&'a str>,
#[props(!optional)]
width: Option<&'a str>,
#[props(!optional)]
height: Option<&'a str>,
}
#[allow(non_snake_case)]
pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> {
let tui_query: Query = cx.consume_context().unwrap();
let tui_query_clone = tui_query.clone();
let text_ref = use_ref(cx, || {
if let Some(intial_text) = cx.props.value {
intial_text.to_string()
} else {
String::new()
}
});
let cursor = use_ref(cx, Cursor::default);
let dragging = use_state(cx, || false);
let text = text_ref.read().clone();
let start_highlight = cursor.read().first().idx(&*text);
let end_highlight = cursor.read().last().idx(&*text);
let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight);
let (text_highlighted, text_after_second_cursor) =
text_after_first_cursor.split_at(end_highlight - start_highlight);
let max_len = cx
.props
.max_length
.as_ref()
.and_then(|s| s.parse().ok())
.unwrap_or(usize::MAX);
let width = cx
.props
.width
.map(|s| s.to_string())
// px is the same as em in tui
.or_else(|| cx.props.size.map(|s| s.to_string() + "px"))
.unwrap_or_else(|| "10px".to_string());
let height = cx.props.height.unwrap_or("3px");
// don't draw a border unless there is enough space
let border = if width
.strip_suffix("px")
.and_then(|w| w.parse::<i32>().ok())
.filter(|w| *w < 3)
.is_some()
|| height
.strip_suffix("px")
.and_then(|h| h.parse::<i32>().ok())
.filter(|h| *h < 3)
.is_some()
{
"none"
} else {
"solid"
};
render! {
div{
width: "{width}",
height: "{height}",
border_style: "{border}",
onkeydown: move |k| {
if k.key() == Key::Enter {
return;
}
let mut text = text_ref.write();
cursor.write().handle_input(&k.code(), &k.key(), &k.modifiers(), &mut *text, max_len);
if let Some(input_handler) = &cx.props.raw_oninput{
input_handler.call(FormData{
value: text.clone(),
values: HashMap::new(),
files: None
});
}
let node = tui_query.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmousemove: move |evt| {
if *dragging.get() {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
if new != cursor.read().start {
cursor.write().end = Some(new);
}
}
},
onmousedown: move |evt| {
let offset = evt.data.element_coordinates();
let mut new = Pos::new(offset.x as usize, offset.y as usize);
if border != "none" {
new.col = new.col.saturating_sub(1);
}
// textboxs are only one line tall
new.row = 0;
new.realize_col(text_ref.read().as_str());
cursor.set(Cursor::from_start(new));
dragging.set(true);
let node = tui_query_clone.get(get_root_id(cx).unwrap());
let Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start;
let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none"));
if let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap();
}
}
else{
execute!(stdout(), MoveTo(x, y)).unwrap();
}
},
onmouseup: move |_| {
dragging.set(false);
},
onmouseleave: move |_| {
dragging.set(false);
},
onmouseenter: move |_| {
dragging.set(false);
},
onfocusout: |_| {
execute!(stdout(), MoveTo(0, 1000)).unwrap();
},
"{text_before_first_cursor}"
span{
background_color: "rgba(255, 255, 255, 50%)",
"{text_highlighted}"
}
"{text_after_second_cursor}"
}
}
}