make TUI widgets cross framework
This commit is contained in:
1 parent
9a16687bdc
commit
c4c35cb046
33 files changed
+3025
-1091
No files matched your search
@@ -1,6 +1,3 @@
|
||||
pub mod prelude;
|
||||
pub mod widgets;
|
||||
|
||||
use std::{
|
||||
ops::Deref,
|
||||
rc::Rc,
|
||||
@@ -8,7 +5,6 @@ use std::{
|
||||
};
|
||||
|
||||
use dioxus_core::{Component, ElementId, VirtualDom};
|
||||
use dioxus_html::EventData;
|
||||
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
|
||||
use dioxus_native_core::prelude::*;
|
||||
|
||||
@@ -83,13 +79,14 @@ impl Driver for DioxusRenderer {
|
||||
rdom: &Arc<RwLock<RealDom>>,
|
||||
id: NodeId,
|
||||
event: &str,
|
||||
value: Rc<EventData>,
|
||||
value: Rc<rink::EventData>,
|
||||
bubbles: bool,
|
||||
) {
|
||||
let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
|
||||
if let Some(id) = id {
|
||||
let inner_value = value.deref().clone();
|
||||
self.vdom
|
||||
.handle_event(event, value.deref().clone().into_any(), id, bubbles);
|
||||
.handle_event(event, inner_value.into_any(), id, bubbles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub use crate::widgets::*;
|
||||
pub use rink::Config;
|
||||
@@ -1,59 +0,0 @@
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
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)",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user