Merge branch 'upstream' into query-system

This commit is contained in:
Evan Almloff committed 2023-04-04 17:36:27 -05:00
commit 2ca5ff96df
97 files changed
+6218 -4877

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+2
View File
@@ -0,0 +1,2 @@
esque
Tui
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "dioxus-tui"
version = "0.2.2"
authors = ["Jonathan Kelley, @dementhos"]
edition = "2021"
description = "TUI-based renderer for Dioxus"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "terminal"]
license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { path = "../dioxus", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0", features = ["dioxus"] }
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
rink = { path = "../rink" }
crossterm = "0.23.0"
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
[dev-dependencies]
dioxus = { path = "../dioxus" }
tokio = { version = "1" }
criterion = "0.3.5"
[[bench]]
name = "update"
harness = false
[features]
default = ["hot-reload"]
hot-reload = ["dioxus-hot-reload"]
+95
View File
@@ -0,0 +1,95 @@
<div align="center">
<h1>Dioxus TUI</h1>
<p>
<strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/dioxus">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/dioxus/actions">
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!--Awesome -->
<a href="https://github.com/dioxuslabs/awesome-dioxus">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<br/>
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
```rust
fn app(cx: Scope) -> Element {
cx.render(rsx!{
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
```
![demo app](examples/example.png)
## Background
You can use Html-like semantics with inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
Dioxus TUI is essentially a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Dioxus TUI doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
## Limitations
- **Subset of Html**
Terminals can only render a subset of HTML. We support as much as we can.
- **Particular frontend design**
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
## Status
**WARNING: Dioxus TUI is currently under construction!**
Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times.
## Features
Dioxus TUI features:
- [x] Flexbox-based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [x] Built-in focusing system
* [x] Widgets<sup>1</sup>
* [ ] Support for events, hooks, and callbacks<sup>2</sup>
* [ ] Html tags<sup>3</sup>
<sup>1</sup> Currently only a subset of the input element is implemented as a component (not an element). The `Input` component supports sliders, text, numbers, passwords, buttons, and checkboxes.
<sup>2</sup> Basic keyboard, mouse, and focus events are implemented.
<sup>3</sup> Currently, most HTML tags don't translate into any meaning inside of Dioxus TUI. So an `input` _element_ won't mean anything nor does it have any additional functionality.
+165
View File
@@ -0,0 +1,165 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
criterion_group!(mbenches, tui_update);
criterion_main!(mbenches);
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn tui_update(c: &mut Criterion) {
{
let mut group = c.benchmark_group("Update boxes");
for size in 1..=20usize {
let parameter_string = format!("{}", (size).pow(2));
group.bench_with_input(
BenchmarkId::new("size", parameter_string),
&size,
|b, size| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(
app,
GridProps {
size: *size,
update_count: 1,
},
Config::default().with_headless(),
)
})
},
);
}
}
{
let mut group = c.benchmark_group("Update many boxes");
for update_count in 1..=20usize {
let update_count = update_count * 20;
let parameter_string = update_count.to_string();
group.bench_with_input(
BenchmarkId::new("update count", parameter_string),
&update_count,
|b, update_count| {
b.iter(|| {
dioxus_tui::launch_cfg_with_props(
app,
GridProps {
size: 20,
update_count: *update_count,
},
Config::default().with_headless(),
)
})
},
);
}
}
}
#[derive(Props, PartialEq)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element {
let count = use_state(cx, || 0);
let x = cx.props.x * 2;
let y = cx.props.y * 2;
let hue = cx.props.hue;
let display_hue = cx.props.hue as u32 / 10;
let count = count.get();
let alpha = cx.props.alpha + (count % 100) as f32;
cx.render(rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
})
}
#[derive(Props, PartialEq)]
struct GridProps {
size: usize,
update_count: usize,
}
#[allow(non_snake_case)]
fn Grid(cx: Scope<GridProps>) -> Element {
let size = cx.props.size;
let count = use_state(cx, || 0);
let counts = use_ref(cx, || vec![0; size * size]);
let ctx: TuiContext = cx.consume_context().unwrap();
if *count.get() + cx.props.update_count >= (size * size) {
ctx.quit();
} else {
for _ in 0..cx.props.update_count {
counts.with_mut(|c| {
let i = *count.current();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
}
render! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..size).map(|x|
{
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..size).map(|y|
{
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
let key = format!("{}-{}", x, y);
rsx! {
Box{
x: x,
y: y,
alpha: 100.0,
hue: alpha,
key: "{key}",
}
}
}
)
}
}
}
)
}
}
}
fn app(cx: Scope<GridProps>) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: cx.props.size,
update_count: cx.props.update_count,
}
}
})
}
@@ -0,0 +1,85 @@
use dioxus::prelude::*;
use dioxus_html::{FocusData, KeyboardData, MouseData, WheelData};
use std::rc::Rc;
fn main() {
dioxus_tui::launch(app);
}
#[derive(Debug)]
enum Event {
MouseMove(Rc<MouseData>),
MouseClick(Rc<MouseData>),
MouseDoubleClick(Rc<MouseData>),
MouseDown(Rc<MouseData>),
MouseUp(Rc<MouseData>),
Wheel(Rc<WheelData>),
KeyDown(Rc<KeyboardData>),
KeyUp(Rc<KeyboardData>),
KeyPress(Rc<KeyboardData>),
FocusIn(Rc<FocusData>),
FocusOut(Rc<FocusData>),
}
const MAX_EVENTS: usize = 8;
fn app(cx: Scope) -> Element {
let events = use_ref(cx, Vec::new);
let events_lock = events.read();
let first_index = events_lock.len().saturating_sub(MAX_EVENTS);
let events_rendered = events_lock[first_index..].iter().map(|event| {
// TUI panics if text overflows (https://github.com/DioxusLabs/dioxus/issues/371)
// temporary hack: just trim the strings (and make sure viewport is big enough)
// todo: remove
let mut trimmed = format!("{event:?}");
trimmed.truncate(200);
rsx!(p { "{trimmed}" })
});
let log_event = move |event: Event| {
events.write().push(event);
};
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "80%",
height: "50%",
border_width: "1px",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
onclick: move |event| log_event(Event::MouseClick(event.inner().clone())),
ondblclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())),
onmouseup: move |event| log_event(Event::MouseUp(event.inner().clone())),
onwheel: move |event| log_event(Event::Wheel(event.inner().clone())),
onkeydown: move |event| log_event(Event::KeyDown(event.inner().clone())),
onkeyup: move |event| log_event(Event::KeyUp(event.inner().clone())),
onkeypress: move |event| log_event(Event::KeyPress(event.inner().clone())),
onfocusin: move |event| log_event(Event::FocusIn(event.inner().clone())),
onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())),
"Hover, click, type or scroll to see the info down below"
},
div {
width: "80%",
height: "50%",
flex_direction: "column",
events_rendered,
},
},
})
}
+27
View File
@@ -0,0 +1,27 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
let radius = use_state(cx, || 0);
cx.render(rsx! {
div {
width: "100%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
onwheel: move |w| radius.modify(|r| (r + w.delta().strip_units().y as i8).abs()),
border_style: "solid none solid double",
border_width: "thick",
border_radius: "{radius}px",
border_color: "#0000FF #FF00FF #FF0000 #00FF00",
"{radius}"
}
})
}
+94
View File
@@ -0,0 +1,94 @@
use dioxus::prelude::*;
use dioxus_html::input_data::keyboard_types::Code;
fn main() {
dioxus_tui::launch(app);
}
#[derive(PartialEq, Props)]
struct ButtonProps {
color_offset: u32,
layer: u16,
}
#[allow(non_snake_case)]
fn Button(cx: Scope<ButtonProps>) -> Element {
let toggle = use_state(cx, || false);
let hovered = use_state(cx, || false);
let hue = cx.props.color_offset % 255;
let saturation = if *toggle.get() { 50 } else { 25 } + if *hovered.get() { 50 } else { 25 };
let brightness = saturation / 2;
let color = format!("hsl({hue}, {saturation}, {brightness})");
cx.render(rsx! {
div{
width: "100%",
height: "100%",
background_color: "{color}",
tabindex: "{cx.props.layer}",
onkeydown: |e| {
if let Code::Space = e.inner().code() {
toggle.modify(|f| !f);
}
},
onclick: |_| {
toggle.modify(|f| !f);
},
onmouseenter: |_|{
hovered.set(true);
},
onmouseleave: |_|{
hovered.set(false);
},
justify_content: "center",
align_items: "center",
display: "flex",
flex_direction: "column",
p{"tabindex: {cx.props.layer}"}
}
})
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
display: "flex",
flex_direction: "column",
width: "100%",
height: "100%",
(1..8).map(|y|
rsx!{
div{
display: "flex",
flex_direction: "row",
width: "100%",
height: "100%",
(1..8).map(|x|{
if (x + y) % 2 == 0{
rsx!{
div{
width: "100%",
height: "100%",
background_color: "rgb(100, 100, 100)",
}
}
}
else{
let layer = (x + y) % 3;
rsx!{
Button{
color_offset: x * y,
layer: layer as u16,
}
}
}
})
}
}
)
}
})
}
@@ -0,0 +1,45 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch_cfg(
app,
dioxus_tui::Config::default().with_rendering_mode(dioxus_tui::RenderingMode::Ansi),
);
}
fn app(cx: Scope) -> Element {
let steps = 50;
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..=steps).map(|x|
{
let hue = x as f32*360.0/steps as f32;
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..=steps).map(|y|
{
let alpha = y as f32*100.0/steps as f32;
rsx! {
div {
left: "{x}px",
top: "{y}px",
width: "10%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
}
}
}
)
}
}
}
)
}
})
}
@@ -0,0 +1,35 @@
use dioxus::core::RenderReturn;
use dioxus::prelude::*;
use dioxus_tui::DioxusElementToNodeId;
use dioxus_tui::Query;
use dioxus_tui::Size;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
let hue = use_state(cx, || 0.0);
let brightness = use_state(cx, || 0.0);
let tui_query: Query = cx.consume_context().unwrap();
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
// disable templates so that every node has an id and can be queried
cx.render(rsx! {
div{
width: "100%",
background_color: "hsl({hue}, 70%, {brightness}%)",
onmousemove: move |evt| {
if let RenderReturn::Ready(node) = cx.root_node() {
if let Some(id) = node.root_ids.get(0){
let node = tui_query.get(mapping.get_node_id(id).unwrap());
let Size{width, height} = node.size().unwrap();
let pos = evt.inner().element_coordinates();
hue.set((pos.x as f32/width as f32)*255.0);
brightness.set((pos.y as f32/height as f32)*100.0);
}
}
},
"hsl({hue}, 70%, {brightness}%)",
}
})
}
@@ -0,0 +1,67 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::default());
}
#[derive(Props, PartialEq)]
struct QuadrentProps {
color: String,
text: String,
}
fn Quadrant(cx: Scope<QuadrentProps>) -> Element {
cx.render(rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "{cx.props.color}",
justify_content: "center",
align_items: "center",
"{cx.props.text}"
}
})
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant{
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant{
color: "black".to_string(),
text: "[B]".to_string()
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant{
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant{
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+82
View File
@@ -0,0 +1,82 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
// justify_content: "center",
// align_items: "center",
// flex_direction: "row",
// background_color: "red",
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
}
})
}
+128
View File
@@ -0,0 +1,128 @@
use dioxus::{events::MouseData, prelude::*};
use dioxus_core::Event;
use std::convert::TryInto;
use std::rc::Rc;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
fn to_str(c: &[i32; 3]) -> String {
"#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::<String>()
}
fn get_brightness(m: &Rc<MouseData>) -> i32 {
let b: i32 = m.held_buttons().len().try_into().unwrap();
127 * b
}
let q1_color = use_state(cx, || [200; 3]);
let q2_color = use_state(cx, || [200; 3]);
let q3_color = use_state(cx, || [200; 3]);
let q4_color = use_state(cx, || [200; 3]);
let q1_color_str = to_str(q1_color);
let q2_color_str = to_str(q2_color);
let q3_color_str = to_str(q3_color);
let q4_color_str = to_str(q4_color);
let page_coordinates = use_state(cx, || "".to_string());
let element_coordinates = use_state(cx, || "".to_string());
let buttons = use_state(cx, || "".to_string());
let modifiers = use_state(cx, || "".to_string());
let update_data = move |event: Event<MouseData>| {
let mouse_data = event.inner();
page_coordinates.set(format!("{:?}", mouse_data.page_coordinates()));
element_coordinates.set(format!("{:?}", mouse_data.element_coordinates()));
// Note: client coordinates are also available, but they would be the same as the page coordinates in this example, because there is no scrolling.
// There are also screen coordinates, but they are currently the same as client coordinates due to technical limitations
buttons.set(format!("{:?}", mouse_data.held_buttons()));
modifiers.set(format!("{:?}", mouse_data.modifiers()));
};
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q1_color_str}",
onmouseenter: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]),
onmousedown: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]),
onmouseup: move |m| q1_color.set([get_brightness(m.inner()), 0, 0]),
onwheel: move |w| q1_color.set([q1_color[0] + (10.0*w.delta().strip_units().y) as i32, 0, 0]),
onmouseleave: move |_| q1_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q2_color_str}",
onmouseenter: move |m| q2_color.set([get_brightness(m.inner()); 3]),
onmousedown: move |m| q2_color.set([get_brightness(m.inner()); 3]),
onmouseup: move |m| q2_color.set([get_brightness(m.inner()); 3]),
onwheel: move |w| q2_color.set([q2_color[0] + (10.0*w.delta().strip_units().y) as i32;3]),
onmouseleave: move |_| q2_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q3_color_str}",
onmouseenter: move |m| q3_color.set([0, get_brightness(m.inner()), 0]),
onmousedown: move |m| q3_color.set([0, get_brightness(m.inner()), 0]),
onmouseup: move |m| q3_color.set([0, get_brightness(m.inner()), 0]),
onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0*w.delta().strip_units().y) as i32, 0]),
onmouseleave: move |_| q3_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q4_color_str}",
onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.inner())]),
onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.inner())]),
onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.inner())]),
onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0*w.delta().strip_units().y) as i32]),
onmouseleave: move |_| q4_color.set([200; 3]),
onmousemove: update_data,
"click me"
}
},
div {"Page coordinates: {page_coordinates}"},
div {"Element coordinates: {element_coordinates}"},
div {"Buttons: {buttons}"},
div {"Modifiers: {modifiers}"},
}
})
}
+28
View File
@@ -0,0 +1,28 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
border_width: "1px",
h1 { height: "2px", color: "green",
"that's awesome!"
}
ul {
flex_direction: "column",
padding_left: "3px",
(0..10).map(|i| rsx!(
"> hello {i}"
))
}
}
})
}
+50
View File
@@ -0,0 +1,50 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
background_color: "black",
margin_right: "10px",
div {
width: "70%",
height: "70%",
background_color: "green",
margin_left: "4px",
div {
width: "100%",
height: "100%",
margin_top: "2px",
margin_bottom: "2px",
margin_left: "2px",
margin_right: "2px",
flex_shrink: "0",
background_color: "red",
justify_content: "center",
align_items: "center",
flex_direction: "column",
padding_top: "2px",
padding_bottom: "2px",
padding_left: "4px",
padding_right: "4px",
"[A]"
"[A]"
"[A]"
"[A]"
}
}
}
})
}
+60
View File
@@ -0,0 +1,60 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "red",
justify_content: "center",
align_items: "center",
"[A]"
}
div {
width: "50%",
height: "100%",
background_color: "black",
justify_content: "center",
align_items: "center",
"[B]"
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
background_color: "green",
justify_content: "center",
align_items: "center",
"[C]"
}
div {
width: "50%",
height: "100%",
background_color: "blue",
justify_content: "center",
align_items: "center",
"[D]"
}
}
}
})
}
+19
View File
@@ -0,0 +1,19 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
+114
View File
@@ -0,0 +1,114 @@
use dioxus::prelude::*;
use dioxus_tui::{Config, TuiContext};
/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time.
fn main() {
for size in 1..=20usize {
for _ in 0..10 {
dioxus_tui::launch_cfg_with_props(app, size, Config::default().with_headless())
}
}
}
#[derive(Props, PartialEq)]
struct BoxProps {
x: usize,
y: usize,
hue: f32,
alpha: f32,
}
#[allow(non_snake_case)]
fn Box(cx: Scope<BoxProps>) -> Element {
let count = use_state(cx, || 0);
let x = cx.props.x * 2;
let y = cx.props.y * 2;
let hue = cx.props.hue;
let display_hue = cx.props.hue as u32 / 10;
let count = count.get();
let alpha = cx.props.alpha + (count % 100) as f32;
cx.render(rsx! {
div {
left: "{x}%",
top: "{y}%",
width: "100%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
align_items: "center",
p{"{display_hue:03}"}
}
})
}
#[derive(Props, PartialEq)]
struct GridProps {
size: usize,
}
#[allow(non_snake_case)]
fn Grid(cx: Scope<GridProps>) -> Element {
let size = cx.props.size;
let count = use_state(cx, || 0);
let counts = use_ref(cx, || vec![0; size * size]);
let ctx: TuiContext = cx.consume_context().unwrap();
if *count.get() + 1 >= (size * size) {
ctx.quit();
} else {
counts.with_mut(|c| {
let i = *count.current();
c[i] += 1;
c[i] %= 360;
});
count.with_mut(|i| {
*i += 1;
*i %= size * size;
});
}
render! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..size).map(|x|
{
rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..size).map(|y|
{
let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32;
let key = format!("{}-{}", x, y);
rsx! {
Box{
x: x,
y: y,
alpha: 100.0,
hue: alpha,
key: "{key}",
}
}
}
)
}
}
}
)
}
}
}
fn app(cx: Scope<usize>) -> Element {
cx.render(rsx! {
div{
width: "100%",
height: "100%",
Grid{
size: *cx.props,
}
}
})
}
+32
View File
@@ -0,0 +1,32 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
let count = use_state(cx, || 0);
use_future(cx, (), move |_| {
let count = count.to_owned();
let update = cx.schedule_update();
async move {
loop {
count.with_mut(|f| *f += 1);
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
update();
}
}
});
cx.render(rsx! {
div { width: "100%",
div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center",
"Hello {count}!"
}
div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center",
"Hello {count}!"
}
}
})
}
+113
View File
@@ -0,0 +1,113 @@
use dioxus::prelude::*;
fn main() {
dioxus_tui::launch(app);
}
fn app(cx: Scope) -> Element {
let alpha = use_state(cx, || 100);
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
onwheel: move |evt| alpha.set((**alpha + evt.inner().delta().strip_units().y as i64).clamp(0, 100)),
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
color: "green",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
div {
font_weight: "bold",
color: "#666666",
p {
"bold"
}
p {
font_weight: "normal",
" normal"
}
}
p {
font_style: "italic",
color: "red",
"italic"
}
p {
text_decoration: "underline",
color: "rgba(255, 255, 255)",
"underline"
}
p {
text_decoration: "line-through",
color: "hsla(10, 100%, 70%)",
"line-through"
}
div{
position: "absolute",
top: "1px",
background_color: "rgba(255, 0, 0, 50%)",
width: "100%",
p {
color: "rgba(255, 255, 255, {alpha}%)",
background_color: "rgba(100, 100, 100, {alpha}%)",
"rgba(255, 255, 255, {alpha}%)"
}
p {
color: "rgba(255, 255, 255, 100%)",
"rgba(255, 255, 255, 100%)"
}
}
}
})
}
+92
View File
@@ -0,0 +1,92 @@
use dioxus::prelude::*;
use dioxus_html::FormData;
use dioxus_tui::prelude::*;
fn main() {
dioxus_tui::launch_cfg(app, Config::new());
}
fn app(cx: Scope) -> Element {
let bg_green = use_state(cx, || false);
let color = if *bg_green.get() { "green" } else { "red" };
cx.render(rsx! {
div{
width: "100%",
background_color: "{color}",
flex_direction: "column",
align_items: "center",
justify_content: "center",
Input{
oninput: |data: FormData| if &data.value == "good"{
bg_green.set(true);
} else{
bg_green.set(false);
},
r#type: "checkbox",
value: "good",
width: "50%",
height: "10%",
checked: "true",
}
Input{
oninput: |data: FormData| if &data.value == "hello world"{
bg_green.set(true);
} else{
bg_green.set(false);
},
width: "50%",
height: "10%",
maxlength: "11",
}
Input{
oninput: |data: FormData| {
if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "range",
width: "50%",
height: "10%",
min: "20",
max: "80",
}
Input{
oninput: |data: FormData| {
if data.value == "10"{
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "number",
width: "50%",
height: "10%",
maxlength: "4",
}
Input{
oninput: |data: FormData| {
if data.value == "hello world"{
bg_green.set(true);
} else{
bg_green.set(false);
}
},
r#type: "password",
width: "50%",
height: "10%",
maxlength: "11",
}
Input{
onclick: |_: FormData| bg_green.set(true),
r#type: "button",
value: "green",
width: "50%",
height: "10%",
}
}
})
}
+99
View File
@@ -0,0 +1,99 @@
use std::{
any::Any,
fmt::{Display, Formatter},
rc::Rc,
};
use dioxus_core::{ElementId, Mutations, VirtualDom};
use dioxus_html::{
geometry::euclid::{Point2D, Rect, Size2D},
MountedData, MountedError, RenderedElementBacking,
};
use dioxus_native_core::NodeId;
use rink::query::{ElementRef, Query};
pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec<ElementId> {
let mut mount_events = Vec::new();
for mutation in &mutations.edits {
if let dioxus_core::Mutation::NewEventListener {
name: "mounted",
id,
} = mutation
{
mount_events.push(*id);
}
}
mount_events
}
// We need to queue the mounted events to give rink time to rendere and resolve the layout of elements after they are created
pub(crate) fn create_mounted_events(
vdom: &VirtualDom,
events: &mut Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
mount_events: impl Iterator<Item = (ElementId, NodeId)>,
) {
let query: Query = vdom
.base_scope()
.consume_context()
.expect("Query should be in context");
for (id, node_id) in mount_events {
let element = TuiElement {
query: query.clone(),
id: node_id,
};
events.push((id, "mounted", Rc::new(MountedData::new(element)), false));
}
}
struct TuiElement {
query: Query,
id: NodeId,
}
impl TuiElement {
pub(crate) fn element(&self) -> ElementRef {
self.query.get(self.id)
}
}
impl RenderedElementBacking for TuiElement {
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let layout = self.element().layout();
Box::pin(async move {
match layout {
Some(layout) => {
let x = layout.location.x as f64;
let y = layout.location.y as f64;
let width = layout.size.width as f64;
let height = layout.size.height as f64;
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
}
None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))),
}
})
}
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
}
#[derive(Debug)]
struct TuiElementNotFound;
impl Display for TuiElementNotFound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TUI element not found")
}
}
impl std::error::Error for TuiElementNotFound {}
+184
View File
@@ -0,0 +1,184 @@
mod element;
pub mod prelude;
pub mod widgets;
use std::{
any::Any,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
};
use dioxus_core::{Component, ElementId, VirtualDom};
use dioxus_html::EventData;
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
use dioxus_native_core::prelude::*;
use element::{create_mounted_events, find_mount_events};
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
use rink::{render, Driver};
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 queued_events = Vec::new();
{
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = dioxus_state.write().unwrap();
// Find any mount events
let mounted = dbg!(find_mount_events(&muts));
dioxus_state.apply_mutations(&mut rdom, muts);
// Send the mount events
create_mounted_events(
&vdom,
&mut queued_events,
mounted
.iter()
.map(|id| (*dbg!(id), dioxus_state.element_to_node_id(*id))),
);
}
DioxusRenderer {
vdom,
dioxus_state,
queued_events,
#[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>>,
// Events that are queued up to be sent to the vdom next time the vdom is polled
queued_events: Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
impl Driver for DioxusRenderer {
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>) {
let muts = self.vdom.render_immediate();
{
let mut rdom = rdom.write().unwrap();
{
// Find any mount events
let mounted = find_mount_events(&muts);
let mut dioxus_state = self.dioxus_state.write().unwrap();
dioxus_state.apply_mutations(&mut rdom, muts);
// Send the mount events
create_mounted_events(
&self.vdom,
&mut self.queued_events,
mounted
.iter()
.map(|id| (*id, dioxus_state.element_to_node_id(*id))),
);
}
}
}
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 = ()> + '_>> {
// Add any queued events
for (id, event, value, bubbles) in self.queued_events.drain(..) {
self.vdom.handle_event(event, value, id, bubbles);
}
#[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}"
}
}
}
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
html,
body {
height: 100%;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: black;
/* justify-content: center;
align-items: center; */
/* margin: auto; */
}
.smaller {
height: 70%;
width: 70%;
background-color: green;
/* justify-content: center; */
/* align-items: center; */
}
.superinner {
height: 100%;
width: 100%;
/* display: flex; */
/* */
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
/* */
background-color: red;
justify-content: center;
align-items: center;
flex-direction: column;
/* margin: 20px; */
/* margin: 20px; */
}
</style>
</head>
<body>
<div class="container">
<div class="smaller">
<div class="superinner">
<h1>Hello World</h1>
<p>This is a test</p>
</div>
</div>
</div>
<!-- <div class="container">
<div class="smaller">
hello world
<div style="color: green; margin: 40px;">
goodbye
<div style="color:red;">
asdasdasd
</div>
</div>
</div>
</div> -->
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
use dioxus::prelude::*;
use dioxus_html::input_data::keyboard_types::Code;
use dioxus_tui::TuiContext;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
/// The tui renderer will look for any event that has occured or any future that has resolved in a loop.
/// It will resolve at most one event per loop.
/// This future will resolve after a certain number of polls. If the number of polls is greater than the number of events triggered, and the event has not been recieved there is an issue with the event system.
struct PollN(usize);
impl PollN {
fn new(n: usize) -> Self {
PollN(n)
}
}
impl Future for PollN {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
if self.0 == 0 {
Poll::Ready(())
} else {
self.0 -= 1;
Poll::Pending
}
}
}
#[test]
fn key_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
// focus the element
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onkeydown: move |evt| {
assert_eq!(evt.data.code(), Code::KeyA);
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(2).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onmousedown: move |evt| {
assert!(evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_up() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onmouseup: move |_| {
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_enter() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_exit() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_move() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 40,
row: 40,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 60,
row: 60,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onmousemove: move |_|{
tui_ctx.quit();
},
}
})
}
}
#[test]
fn wheel() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::ScrollDown,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onwheel: move |evt| {
assert!(evt.data.delta().strip_units().y > 0.0);
tui_ctx.quit();
},
}
})
}
}
#[test]
fn click() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onclick: move |_|{
tui_ctx.quit();
},
}
})
}
}
#[test]
fn context_menu() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
PollN::new(3).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
oncontextmenu: move |_|{
tui_ctx.quit();
},
}
})
}
}