fix native core, plasmo and dioxus-tui
This commit is contained in:
1 parent
87572d694f
commit
439e39bab4
37 files changed
+588
-266
No files matched your search
@@ -0,0 +1,58 @@
|
||||
#[derive(Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub struct Config {
|
||||
pub(crate) rendering_mode: RenderingMode,
|
||||
/// Controls if the terminal quit when the user presses `ctrl+c`?
|
||||
/// To handle quiting on your own, use the [crate::TuiContext] root context.
|
||||
pub(crate) ctrl_c_quit: bool,
|
||||
/// Controls if the terminal should dislay anything, usefull for testing.
|
||||
pub(crate) headless: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_rendering_mode(self, rendering_mode: RenderingMode) -> Self {
|
||||
Self {
|
||||
rendering_mode,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_ctrl_c_quit(self) -> Self {
|
||||
Self {
|
||||
ctrl_c_quit: false,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_headless(self) -> Self {
|
||||
Self {
|
||||
headless: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rendering_mode: Default::default(),
|
||||
ctrl_c_quit: true,
|
||||
headless: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub enum RenderingMode {
|
||||
/// only 16 colors by accessed by name, no alpha support
|
||||
BaseColors,
|
||||
/// 8 bit colors, will be downsampled from rgb colors
|
||||
Ansi,
|
||||
/// 24 bit colors, most terminals support this
|
||||
#[default]
|
||||
Rgb,
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
use crate::prevent_default::PreventDefault;
|
||||
|
||||
use dioxus_native_core::{
|
||||
node_ref::{AttributeMaskBuilder, NodeMaskBuilder},
|
||||
prelude::*,
|
||||
real_dom::NodeImmutable,
|
||||
utils::{IteratorMovement, PersistantElementIter},
|
||||
};
|
||||
use dioxus_native_core_macro::partial_derive_state;
|
||||
use once_cell::sync::Lazy;
|
||||
use rustc_hash::FxHashSet;
|
||||
use shipyard::Component;
|
||||
use shipyard::{Get, ViewMut};
|
||||
|
||||
use std::{cmp::Ordering, num::NonZeroU16};
|
||||
|
||||
use dioxus_native_core::node_ref::NodeView;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Focused(pub bool);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub(crate) enum FocusLevel {
|
||||
#[default]
|
||||
Unfocusable,
|
||||
Focusable,
|
||||
Ordered(std::num::NonZeroU16),
|
||||
}
|
||||
|
||||
impl FocusLevel {
|
||||
pub fn focusable(&self) -> bool {
|
||||
match self {
|
||||
FocusLevel::Unfocusable => false,
|
||||
FocusLevel::Focusable => true,
|
||||
FocusLevel::Ordered(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for FocusLevel {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FocusLevel {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match (self, other) {
|
||||
(FocusLevel::Unfocusable, FocusLevel::Unfocusable) => std::cmp::Ordering::Equal,
|
||||
(FocusLevel::Unfocusable, FocusLevel::Focusable) => std::cmp::Ordering::Less,
|
||||
(FocusLevel::Unfocusable, FocusLevel::Ordered(_)) => std::cmp::Ordering::Less,
|
||||
(FocusLevel::Focusable, FocusLevel::Unfocusable) => std::cmp::Ordering::Greater,
|
||||
(FocusLevel::Focusable, FocusLevel::Focusable) => std::cmp::Ordering::Equal,
|
||||
(FocusLevel::Focusable, FocusLevel::Ordered(_)) => std::cmp::Ordering::Greater,
|
||||
(FocusLevel::Ordered(_), FocusLevel::Unfocusable) => std::cmp::Ordering::Greater,
|
||||
(FocusLevel::Ordered(_), FocusLevel::Focusable) => std::cmp::Ordering::Less,
|
||||
(FocusLevel::Ordered(a), FocusLevel::Ordered(b)) => a.cmp(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default, Component)]
|
||||
pub(crate) struct Focus {
|
||||
pub level: FocusLevel,
|
||||
}
|
||||
|
||||
#[partial_derive_state]
|
||||
impl State for Focus {
|
||||
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
|
||||
.with_attrs(AttributeMaskBuilder::Some(FOCUS_ATTRIBUTES))
|
||||
.with_listeners();
|
||||
|
||||
type ParentDependencies = ();
|
||||
type ChildDependencies = ();
|
||||
type NodeDependencies = ();
|
||||
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
node_view: NodeView,
|
||||
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: &SendAnyMap,
|
||||
) -> bool {
|
||||
let new = Focus {
|
||||
level: if let Some(a) = node_view
|
||||
.attributes()
|
||||
.and_then(|mut a| a.find(|a| a.attribute.name == "tabindex"))
|
||||
{
|
||||
if let Some(index) = a
|
||||
.value
|
||||
.as_int()
|
||||
.or_else(|| a.value.as_text().and_then(|v| v.parse::<i64>().ok()))
|
||||
{
|
||||
match index.cmp(&0) {
|
||||
Ordering::Less => FocusLevel::Unfocusable,
|
||||
Ordering::Equal => FocusLevel::Focusable,
|
||||
Ordering::Greater => {
|
||||
FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FocusLevel::Unfocusable
|
||||
}
|
||||
} else if node_view
|
||||
.listeners()
|
||||
.and_then(|mut listeners| {
|
||||
listeners.any(|l| FOCUS_EVENTS.contains(&l)).then_some(())
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
FocusLevel::Focusable
|
||||
} else {
|
||||
FocusLevel::Unfocusable
|
||||
},
|
||||
};
|
||||
if *self != new {
|
||||
*self = new;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn create<'a>(
|
||||
node_view: NodeView<()>,
|
||||
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
context: &SendAnyMap,
|
||||
) -> Self {
|
||||
let mut myself = Self::default();
|
||||
myself.update(node_view, node, parent, children, context);
|
||||
myself
|
||||
}
|
||||
}
|
||||
|
||||
static FOCUS_EVENTS: Lazy<FxHashSet<&str>> =
|
||||
Lazy::new(|| ["keydown", "keypress", "keyup"].into_iter().collect());
|
||||
const FOCUS_ATTRIBUTES: &[&str] = &["tabindex"];
|
||||
|
||||
pub(crate) struct FocusState {
|
||||
pub(crate) focus_iter: PersistantElementIter,
|
||||
pub(crate) last_focused_id: Option<NodeId>,
|
||||
pub(crate) focus_level: FocusLevel,
|
||||
pub(crate) dirty: bool,
|
||||
}
|
||||
|
||||
impl FocusState {
|
||||
pub fn create(rdom: &mut RealDom) -> Self {
|
||||
let focus_iter = PersistantElementIter::create(rdom);
|
||||
Self {
|
||||
focus_iter,
|
||||
last_focused_id: Default::default(),
|
||||
focus_level: Default::default(),
|
||||
dirty: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the focus has changed.
|
||||
pub fn progress(&mut self, rdom: &mut RealDom, forward: bool) -> bool {
|
||||
if let Some(last) = self.last_focused_id {
|
||||
if rdom.get(last).unwrap().get::<PreventDefault>().map(|p| *p)
|
||||
== Some(PreventDefault::KeyDown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// the id that started focused to track when a loop has happened
|
||||
let mut loop_marker_id = self.last_focused_id;
|
||||
let focus_level = &mut self.focus_level;
|
||||
let mut next_focus = None;
|
||||
|
||||
loop {
|
||||
let new = if forward {
|
||||
self.focus_iter.next(rdom)
|
||||
} else {
|
||||
self.focus_iter.prev(rdom)
|
||||
};
|
||||
let new_id = new.id();
|
||||
if let IteratorMovement::Looped = new.movement() {
|
||||
let mut closest_level = None;
|
||||
|
||||
if forward {
|
||||
// find the closest focusable element after the current level
|
||||
rdom.traverse_depth_first(|n| {
|
||||
let node_level = n.get::<Focus>().unwrap().level;
|
||||
if node_level != *focus_level
|
||||
&& node_level.focusable()
|
||||
&& node_level > *focus_level
|
||||
{
|
||||
if let Some(level) = &mut closest_level {
|
||||
if node_level < *level {
|
||||
*level = node_level;
|
||||
}
|
||||
} else {
|
||||
closest_level = Some(node_level);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// find the closest focusable element before the current level
|
||||
rdom.traverse_depth_first(|n| {
|
||||
let node_level = n.get::<Focus>().unwrap().level;
|
||||
if node_level != *focus_level
|
||||
&& node_level.focusable()
|
||||
&& node_level < *focus_level
|
||||
{
|
||||
if let Some(level) = &mut closest_level {
|
||||
if node_level > *level {
|
||||
*level = node_level;
|
||||
}
|
||||
} else {
|
||||
closest_level = Some(node_level);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// extend the loop_marker_id to allow for another pass
|
||||
loop_marker_id = None;
|
||||
|
||||
if let Some(level) = closest_level {
|
||||
*focus_level = level;
|
||||
} else if forward {
|
||||
*focus_level = FocusLevel::Unfocusable;
|
||||
} else {
|
||||
*focus_level = FocusLevel::Focusable;
|
||||
}
|
||||
}
|
||||
|
||||
// once we have looked at all the elements exit the loop
|
||||
if let Some(last) = loop_marker_id {
|
||||
if new_id == last {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
loop_marker_id = Some(new_id);
|
||||
}
|
||||
|
||||
let current_level = rdom.get(new_id).unwrap().get::<Focus>().unwrap().level;
|
||||
let after_previous_focused = if forward {
|
||||
current_level >= *focus_level
|
||||
} else {
|
||||
current_level <= *focus_level
|
||||
};
|
||||
if after_previous_focused && current_level.focusable() && current_level == *focus_level
|
||||
{
|
||||
next_focus = Some(new_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = next_focus {
|
||||
let mut node = rdom.get_mut(id).unwrap();
|
||||
if !node.get::<Focus>().unwrap().level.focusable() {
|
||||
panic!()
|
||||
}
|
||||
node.insert(Focused(true));
|
||||
if let Some(old) = self.last_focused_id.replace(id) {
|
||||
let mut focused_borrow: ViewMut<Focused> = rdom.raw_world().borrow().unwrap();
|
||||
let focused = (&mut focused_borrow).get(old).unwrap();
|
||||
focused.0 = false;
|
||||
}
|
||||
// reset the position to the currently focused element
|
||||
while self.focus_iter.next(rdom).id() != id {}
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn set_focus(&mut self, rdom: &mut RealDom, id: NodeId) {
|
||||
if let Some(old) = self.last_focused_id.replace(id) {
|
||||
let mut node = rdom.get_mut(old).unwrap();
|
||||
node.insert(Focused(false));
|
||||
}
|
||||
let mut node = rdom.get_mut(id).unwrap();
|
||||
node.insert(Focused(true));
|
||||
self.focus_level = node.get::<Focus>().unwrap().level;
|
||||
// reset the position to the currently focused element
|
||||
while self.focus_iter.next(rdom).id() != id {}
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
pub(crate) fn clean(&mut self) -> bool {
|
||||
let old = self.dirty;
|
||||
self.dirty = false;
|
||||
old
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,380 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use dioxus_native_core::exports::shipyard::Component;
|
||||
use dioxus_native_core::layout_attributes::{
|
||||
apply_layout_attributes_cfg, BorderWidths, LayoutConfigeration,
|
||||
};
|
||||
use dioxus_native_core::node::OwnedAttributeView;
|
||||
use dioxus_native_core::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView};
|
||||
use dioxus_native_core::prelude::*;
|
||||
use dioxus_native_core_macro::partial_derive_state;
|
||||
use taffy::prelude::*;
|
||||
|
||||
use crate::{screen_to_layout_space, unit_to_layout_space};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum PossiblyUninitalized<T> {
|
||||
Uninitalized,
|
||||
Initialized(T),
|
||||
}
|
||||
|
||||
impl<T> PossiblyUninitalized<T> {
|
||||
pub fn unwrap(self) -> T {
|
||||
match self {
|
||||
Self::Initialized(i) => i,
|
||||
_ => panic!("uninitalized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Default for PossiblyUninitalized<T> {
|
||||
fn default() -> Self {
|
||||
Self::Uninitalized
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Default, Debug, Component)]
|
||||
pub(crate) struct TaffyLayout {
|
||||
pub style: Style,
|
||||
pub node: PossiblyUninitalized<Node>,
|
||||
}
|
||||
|
||||
#[partial_derive_state]
|
||||
impl State for TaffyLayout {
|
||||
type ChildDependencies = (Self,);
|
||||
type ParentDependencies = ();
|
||||
type NodeDependencies = ();
|
||||
|
||||
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
|
||||
.with_attrs(AttributeMaskBuilder::Some(SORTED_LAYOUT_ATTRS))
|
||||
.with_text();
|
||||
|
||||
// The layout state should be effected by the shadow dom
|
||||
const TRAVERSE_SHADOW_DOM: bool = true;
|
||||
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
node_view: NodeView,
|
||||
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
ctx: &SendAnyMap,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
let taffy: &Arc<Mutex<Taffy>> = ctx.get().unwrap();
|
||||
let mut taffy = taffy.lock().expect("poisoned taffy");
|
||||
let mut style = Style::default();
|
||||
if let Some(text) = node_view.text() {
|
||||
let char_len = text.chars().count();
|
||||
|
||||
style = Style {
|
||||
size: Size {
|
||||
// characters are 1 point tall
|
||||
height: Dimension::Points(screen_to_layout_space(1)),
|
||||
|
||||
// text is as long as it is declared
|
||||
width: Dimension::Points(screen_to_layout_space(char_len as u16)),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
if let PossiblyUninitalized::Initialized(n) = self.node {
|
||||
if self.style != style {
|
||||
taffy.set_style(n, style.clone()).unwrap();
|
||||
}
|
||||
} else {
|
||||
self.node =
|
||||
PossiblyUninitalized::Initialized(taffy.new_leaf(style.clone()).unwrap());
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
// gather up all the styles from the attribute list
|
||||
if let Some(attributes) = node_view.attributes() {
|
||||
for OwnedAttributeView {
|
||||
attribute, value, ..
|
||||
} in attributes
|
||||
{
|
||||
if value.as_custom().is_none() {
|
||||
apply_layout_attributes_cfg(
|
||||
&attribute.name,
|
||||
&value.to_string(),
|
||||
&mut style,
|
||||
&LayoutConfigeration {
|
||||
border_widths: BorderWidths {
|
||||
thin: 1.0,
|
||||
medium: 1.0,
|
||||
thick: 1.0,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set all direct nodes as our children
|
||||
let mut child_layout = vec![];
|
||||
for (l,) in children {
|
||||
child_layout.push(l.node.unwrap());
|
||||
}
|
||||
|
||||
fn scale_dimension(d: Dimension) -> Dimension {
|
||||
match d {
|
||||
Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
|
||||
Dimension::Percent(p) => Dimension::Percent(p),
|
||||
Dimension::Auto => Dimension::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_length_percentage_auto(d: LengthPercentageAuto) -> LengthPercentageAuto {
|
||||
match d {
|
||||
LengthPercentageAuto::Points(p) => {
|
||||
LengthPercentageAuto::Points(unit_to_layout_space(p))
|
||||
}
|
||||
LengthPercentageAuto::Percent(p) => LengthPercentageAuto::Percent(p),
|
||||
LengthPercentageAuto::Auto => LengthPercentageAuto::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_length_percentage(d: LengthPercentage) -> LengthPercentage {
|
||||
match d {
|
||||
LengthPercentage::Points(p) => {
|
||||
LengthPercentage::Points(unit_to_layout_space(p))
|
||||
}
|
||||
LengthPercentage::Percent(p) => LengthPercentage::Percent(p),
|
||||
}
|
||||
}
|
||||
|
||||
let scaled_style = Style {
|
||||
inset: Rect {
|
||||
left: scale_length_percentage_auto(style.inset.left),
|
||||
right: scale_length_percentage_auto(style.inset.right),
|
||||
top: scale_length_percentage_auto(style.inset.top),
|
||||
bottom: scale_length_percentage_auto(style.inset.bottom),
|
||||
},
|
||||
margin: Rect {
|
||||
left: scale_length_percentage_auto(style.margin.left),
|
||||
right: scale_length_percentage_auto(style.margin.right),
|
||||
top: scale_length_percentage_auto(style.margin.top),
|
||||
bottom: scale_length_percentage_auto(style.margin.bottom),
|
||||
},
|
||||
padding: Rect {
|
||||
left: scale_length_percentage(style.padding.left),
|
||||
right: scale_length_percentage(style.padding.right),
|
||||
top: scale_length_percentage(style.padding.top),
|
||||
bottom: scale_length_percentage(style.padding.bottom),
|
||||
},
|
||||
border: Rect {
|
||||
left: scale_length_percentage(style.border.left),
|
||||
right: scale_length_percentage(style.border.right),
|
||||
top: scale_length_percentage(style.border.top),
|
||||
bottom: scale_length_percentage(style.border.bottom),
|
||||
},
|
||||
gap: Size {
|
||||
width: scale_length_percentage(style.gap.width),
|
||||
height: scale_length_percentage(style.gap.height),
|
||||
},
|
||||
flex_basis: scale_dimension(style.flex_basis),
|
||||
size: Size {
|
||||
width: scale_dimension(style.size.width),
|
||||
height: scale_dimension(style.size.height),
|
||||
},
|
||||
min_size: Size {
|
||||
width: scale_dimension(style.min_size.width),
|
||||
height: scale_dimension(style.min_size.height),
|
||||
},
|
||||
max_size: Size {
|
||||
width: scale_dimension(style.max_size.width),
|
||||
height: scale_dimension(style.max_size.height),
|
||||
},
|
||||
..style.clone()
|
||||
};
|
||||
|
||||
if let PossiblyUninitalized::Initialized(n) = self.node {
|
||||
if self.style != style {
|
||||
taffy.set_style(n, scaled_style).unwrap();
|
||||
}
|
||||
if taffy.children(n).unwrap() != child_layout {
|
||||
taffy.set_children(n, &child_layout).unwrap();
|
||||
}
|
||||
} else {
|
||||
self.node = PossiblyUninitalized::Initialized(
|
||||
taffy
|
||||
.new_with_children(scaled_style, &child_layout)
|
||||
.unwrap(),
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if self.style != style {
|
||||
changed = true;
|
||||
self.style = style;
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn create<'a>(
|
||||
node_view: NodeView<()>,
|
||||
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
context: &SendAnyMap,
|
||||
) -> Self {
|
||||
let mut myself = Self::default();
|
||||
myself.update(node_view, node, parent, children, context);
|
||||
myself
|
||||
}
|
||||
}
|
||||
|
||||
// these are the attributes in layout_attiributes in native-core
|
||||
const SORTED_LAYOUT_ATTRS: &[&str] = &[
|
||||
"align-content",
|
||||
"align-items",
|
||||
"align-self",
|
||||
"animation",
|
||||
"animation-delay",
|
||||
"animation-direction",
|
||||
"animation-duration",
|
||||
"animation-fill-mode",
|
||||
"animation-iteration-count",
|
||||
"animation-name",
|
||||
"animation-play-state",
|
||||
"animation-timing-function",
|
||||
"backface-visibility",
|
||||
"border",
|
||||
"border-bottom",
|
||||
"border-bottom-color",
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-bottom-style",
|
||||
"border-bottom-width",
|
||||
"border-collapse",
|
||||
"border-color",
|
||||
"border-image",
|
||||
"border-image-outset",
|
||||
"border-image-repeat",
|
||||
"border-image-slice",
|
||||
"border-image-source",
|
||||
"border-image-width",
|
||||
"border-left",
|
||||
"border-left-color",
|
||||
"border-left-style",
|
||||
"border-left-width",
|
||||
"border-radius",
|
||||
"border-right",
|
||||
"border-right-color",
|
||||
"border-right-style",
|
||||
"border-right-width",
|
||||
"border-spacing",
|
||||
"border-style",
|
||||
"border-top",
|
||||
"border-top-color",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"border-top-style",
|
||||
"border-top-width",
|
||||
"border-width",
|
||||
"bottom",
|
||||
"box-shadow",
|
||||
"box-sizing",
|
||||
"caption-side",
|
||||
"clear",
|
||||
"clip",
|
||||
"column-count",
|
||||
"column-fill",
|
||||
"column-gap",
|
||||
"column-rule",
|
||||
"column-rule-color",
|
||||
"column-rule-style",
|
||||
"column-rule-width",
|
||||
"column-span",
|
||||
"column-width",
|
||||
"columns",
|
||||
"content",
|
||||
"counter-increment",
|
||||
"counter-reset",
|
||||
"cursor",
|
||||
"direction",
|
||||
"ltr",
|
||||
"rtl",
|
||||
"display",
|
||||
"empty-cells",
|
||||
"flex",
|
||||
"flex-basis",
|
||||
"flex-direction",
|
||||
"flex-flow",
|
||||
"flex-grow",
|
||||
"flex-shrink",
|
||||
"flex-wrap",
|
||||
"float",
|
||||
"height",
|
||||
"justify-content",
|
||||
"flex-start",
|
||||
"flex-end",
|
||||
"center",
|
||||
"space-between",
|
||||
"space-around",
|
||||
"space-evenly",
|
||||
"left",
|
||||
"letter-spacing",
|
||||
"line-height",
|
||||
"list-style",
|
||||
"list-style-image",
|
||||
"list-style-position",
|
||||
"list-style-type",
|
||||
"margin",
|
||||
"margin-bottom",
|
||||
"margin-left",
|
||||
"margin-right",
|
||||
"margin-top",
|
||||
"max-height",
|
||||
"max-width",
|
||||
"min-height",
|
||||
"min-width",
|
||||
"opacity",
|
||||
"order",
|
||||
"outline",
|
||||
"outline-color",
|
||||
"outline-offset",
|
||||
"outline-style",
|
||||
"outline-width",
|
||||
"overflow",
|
||||
"overflow-x",
|
||||
"overflow-y",
|
||||
"padding",
|
||||
"padding-bottom",
|
||||
"padding-left",
|
||||
"padding-right",
|
||||
"padding-top",
|
||||
"page-break-after",
|
||||
"page-break-before",
|
||||
"page-break-inside",
|
||||
"perspective",
|
||||
"perspective-origin",
|
||||
"position",
|
||||
"static",
|
||||
"relative",
|
||||
"fixed",
|
||||
"absolute",
|
||||
"sticky",
|
||||
"pointer-events",
|
||||
"quotes",
|
||||
"resize",
|
||||
"right",
|
||||
"tab-size",
|
||||
"table-layout",
|
||||
"top",
|
||||
"transform",
|
||||
"transform-origin",
|
||||
"transform-style",
|
||||
"transition",
|
||||
"transition-delay",
|
||||
"transition-duration",
|
||||
"transition-property",
|
||||
"transition-timing-function",
|
||||
"vertical-align",
|
||||
"visibility",
|
||||
"white-space",
|
||||
"width",
|
||||
"word-break",
|
||||
"word-spacing",
|
||||
"word-wrap",
|
||||
"z-index",
|
||||
];
|
||||
@@ -0,0 +1,395 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
use crate::focus::Focus;
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
cursor::{MoveTo, RestorePosition, SavePosition, Show},
|
||||
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use dioxus_native_core::{prelude::*, tree::TreeRef};
|
||||
use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap};
|
||||
use focus::FocusState;
|
||||
use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
|
||||
use futures_channel::mpsc::unbounded;
|
||||
use layout::TaffyLayout;
|
||||
use prevent_default::PreventDefault;
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::{io, time::Duration};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::{rc::Rc, sync::RwLock};
|
||||
use style_attributes::StyleModifier;
|
||||
pub use taffy::{geometry::Point, prelude::*};
|
||||
use tokio::select;
|
||||
use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
|
||||
|
||||
mod config;
|
||||
mod focus;
|
||||
mod hooks;
|
||||
mod layout;
|
||||
mod prevent_default;
|
||||
pub mod query;
|
||||
mod render;
|
||||
mod style;
|
||||
mod style_attributes;
|
||||
mod widget;
|
||||
mod widgets;
|
||||
|
||||
pub use config::*;
|
||||
pub use hooks::*;
|
||||
pub use query::Query;
|
||||
|
||||
// the layout space has a multiplier of 10 to minimize rounding errors
|
||||
pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
|
||||
screen as f32 * 10.0
|
||||
}
|
||||
|
||||
pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
|
||||
screen * 10.0
|
||||
}
|
||||
|
||||
pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
|
||||
layout / 10.0
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TuiContext {
|
||||
tx: UnboundedSender<InputEvent>,
|
||||
}
|
||||
|
||||
impl TuiContext {
|
||||
pub fn new(tx: UnboundedSender<InputEvent>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
pub fn quit(&self) {
|
||||
self.tx.unbounded_send(InputEvent::Close).unwrap();
|
||||
}
|
||||
|
||||
pub fn inject_event(&self, event: crossterm::event::Event) {
|
||||
self.tx
|
||||
.unbounded_send(InputEvent::UserInput(event))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render<R: Driver>(
|
||||
cfg: Config,
|
||||
create_renderer: impl FnOnce(
|
||||
&Arc<RwLock<RealDom>>,
|
||||
&Arc<Mutex<Taffy>>,
|
||||
UnboundedSender<InputEvent>,
|
||||
) -> R,
|
||||
) -> Result<()> {
|
||||
let mut rdom = RealDom::new([
|
||||
TaffyLayout::to_type_erased(),
|
||||
Focus::to_type_erased(),
|
||||
StyleModifier::to_type_erased(),
|
||||
PreventDefault::to_type_erased(),
|
||||
]);
|
||||
|
||||
// Setup input handling
|
||||
|
||||
// The event channel for fully resolved events
|
||||
let (event_tx, mut event_reciever) = unbounded();
|
||||
|
||||
// The event channel for raw terminal events
|
||||
let (raw_event_tx, mut raw_event_reciever) = unbounded();
|
||||
let event_tx_clone = raw_event_tx.clone();
|
||||
if !cfg.headless {
|
||||
std::thread::spawn(move || {
|
||||
// Timeout after 10ms when waiting for events
|
||||
let tick_rate = Duration::from_millis(10);
|
||||
loop {
|
||||
if crossterm::event::poll(tick_rate).unwrap() {
|
||||
let evt = crossterm::event::read().unwrap();
|
||||
if raw_event_tx
|
||||
.unbounded_send(InputEvent::UserInput(evt))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
register_widgets(&mut rdom, event_tx);
|
||||
|
||||
let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
|
||||
|
||||
let rdom = Arc::new(RwLock::new(rdom));
|
||||
let taffy = Arc::new(Mutex::new(Taffy::new()));
|
||||
let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone);
|
||||
|
||||
// insert the query engine into the rdom
|
||||
let query_engine = Query::new(rdom.clone(), taffy.clone());
|
||||
{
|
||||
let mut rdom = rdom.write().unwrap();
|
||||
rdom.raw_world_mut().add_unique(query_engine);
|
||||
}
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(async {
|
||||
{
|
||||
renderer.update(&rdom);
|
||||
let mut any_map = SendAnyMap::new();
|
||||
any_map.insert(taffy.clone());
|
||||
let mut rdom = rdom.write().unwrap();
|
||||
let _ = rdom.update_state(any_map);
|
||||
}
|
||||
|
||||
let mut terminal = (!cfg.headless).then(|| {
|
||||
enable_raw_mode().unwrap();
|
||||
let mut stdout = std::io::stdout();
|
||||
execute!(
|
||||
stdout,
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture,
|
||||
MoveTo(0, 1000)
|
||||
)
|
||||
.unwrap();
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
Terminal::new(backend).unwrap()
|
||||
});
|
||||
if let Some(terminal) = &mut terminal {
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
let mut to_rerender = FxDashSet::default();
|
||||
to_rerender.insert(rdom.read().unwrap().root_id());
|
||||
let mut updated = true;
|
||||
|
||||
loop {
|
||||
/*
|
||||
-> render the nodes in the right place with tui/crossterm
|
||||
-> wait for changes
|
||||
-> resolve events
|
||||
-> lazily update the layout and style based on nodes changed
|
||||
use simd to compare lines for diffing?
|
||||
todo: lazy re-rendering
|
||||
*/
|
||||
|
||||
if !to_rerender.is_empty() || updated {
|
||||
updated = false;
|
||||
fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
|
||||
let width = screen_to_layout_space(dims.width);
|
||||
let height = screen_to_layout_space(dims.height);
|
||||
let root_node = rdom
|
||||
.get(rdom.root_id())
|
||||
.unwrap()
|
||||
.get::<TaffyLayout>()
|
||||
.unwrap()
|
||||
.node
|
||||
.unwrap();
|
||||
|
||||
// the root node fills the entire area
|
||||
let mut style = taffy.style(root_node).unwrap().clone();
|
||||
let new_size = Size {
|
||||
width: Dimension::Points(width),
|
||||
height: Dimension::Points(height),
|
||||
};
|
||||
if style.size != new_size {
|
||||
style.size = new_size;
|
||||
taffy.set_style(root_node, style).unwrap();
|
||||
}
|
||||
|
||||
let size = Size {
|
||||
width: AvailableSpace::Definite(width),
|
||||
height: AvailableSpace::Definite(height),
|
||||
};
|
||||
taffy.compute_layout(root_node, size).unwrap();
|
||||
}
|
||||
if let Some(terminal) = &mut terminal {
|
||||
execute!(terminal.backend_mut(), SavePosition).unwrap();
|
||||
terminal.draw(|frame| {
|
||||
let rdom = rdom.write().unwrap();
|
||||
let mut taffy = taffy.lock().expect("taffy lock poisoned");
|
||||
// size is guaranteed to not change when rendering
|
||||
resize(frame.size(), &mut taffy, &rdom);
|
||||
let root = rdom.get(rdom.root_id()).unwrap();
|
||||
render::render_vnode(frame, &taffy, root, cfg, Point::ZERO);
|
||||
})?;
|
||||
execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
|
||||
} else {
|
||||
let rdom = rdom.read().unwrap();
|
||||
resize(
|
||||
ratatui::layout::Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
&mut taffy.lock().expect("taffy lock poisoned"),
|
||||
&rdom,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut event_recieved = None;
|
||||
{
|
||||
let wait = renderer.poll_async();
|
||||
|
||||
pin_mut!(wait);
|
||||
|
||||
select! {
|
||||
_ = wait => {
|
||||
|
||||
},
|
||||
evt = raw_event_reciever.next() => {
|
||||
match evt.as_ref().unwrap() {
|
||||
InputEvent::UserInput(event) => match event {
|
||||
TermEvent::Key(key) => {
|
||||
if matches!(key.code, KeyCode::Char('C' | 'c'))
|
||||
&& key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
&& cfg.ctrl_c_quit
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
TermEvent::Resize(_, _) => updated = true,
|
||||
_ => {}
|
||||
},
|
||||
InputEvent::Close => break,
|
||||
};
|
||||
|
||||
if let InputEvent::UserInput(evt) = evt.unwrap() {
|
||||
register_event(evt);
|
||||
}
|
||||
},
|
||||
Some(evt) = event_reciever.next() => {
|
||||
event_recieved = Some(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(evt) = event_recieved {
|
||||
renderer.handle_event(
|
||||
&rdom,
|
||||
evt.id,
|
||||
evt.name,
|
||||
Rc::new(evt.data),
|
||||
evt.bubbles,
|
||||
);
|
||||
}
|
||||
{
|
||||
let evts = handler.get_events(
|
||||
&taffy.lock().expect("taffy lock poisoned"),
|
||||
&mut rdom.write().unwrap(),
|
||||
);
|
||||
updated |= handler.state().focus_state.clean();
|
||||
|
||||
for e in evts {
|
||||
bubble_event_to_widgets(&mut rdom.write().unwrap(), &e);
|
||||
renderer.handle_event(&rdom, e.id, e.name, Rc::new(e.data), e.bubbles);
|
||||
}
|
||||
}
|
||||
// updates the dom's nodes
|
||||
renderer.update(&rdom);
|
||||
// update the style and layout
|
||||
let mut rdom = rdom.write().unwrap();
|
||||
let mut any_map = SendAnyMap::new();
|
||||
any_map.insert(taffy.clone());
|
||||
let (new_to_rerender, dirty) = rdom.update_state(any_map);
|
||||
to_rerender = new_to_rerender;
|
||||
let text_mask = NodeMaskBuilder::new().with_text().build();
|
||||
for (id, mask) in dirty {
|
||||
if mask.overlaps(&text_mask) {
|
||||
to_rerender.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(terminal) = &mut terminal {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputEvent {
|
||||
UserInput(TermEvent),
|
||||
Close,
|
||||
}
|
||||
|
||||
pub trait Driver {
|
||||
fn update(&mut self, rdom: &Arc<RwLock<RealDom>>);
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
rdom: &Arc<RwLock<RealDom>>,
|
||||
id: NodeId,
|
||||
event: &str,
|
||||
value: Rc<EventData>,
|
||||
bubbles: bool,
|
||||
);
|
||||
fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
|
||||
}
|
||||
|
||||
/// Before sending the event to drivers, we need to bubble it up the tree to any widgets that are listening
|
||||
fn bubble_event_to_widgets(rdom: &mut RealDom, event: &Event) {
|
||||
let id = event.id;
|
||||
let mut node = Some(id);
|
||||
|
||||
while let Some(node_id) = node {
|
||||
let parent_id = {
|
||||
let tree = rdom.tree_ref();
|
||||
tree.parent_id_advanced(node_id, true)
|
||||
};
|
||||
|
||||
{
|
||||
// println!("@ bubbling event to node {:?}", node_id);
|
||||
let mut node_mut = rdom.get_mut(node_id).unwrap();
|
||||
if let Some(mut widget) = node_mut
|
||||
.get_mut::<RinkWidgetTraitObject>()
|
||||
.map(|w| w.clone())
|
||||
{
|
||||
widget.handle_event(event, node_mut)
|
||||
}
|
||||
}
|
||||
|
||||
if !event.bubbles {
|
||||
// println!("event does not bubble");
|
||||
break;
|
||||
}
|
||||
node = parent_id;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout {
|
||||
let mut node_layout = *taffy
|
||||
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
|
||||
.unwrap();
|
||||
let mut current = node;
|
||||
|
||||
let dom = node.real_dom();
|
||||
let tree = dom.tree_ref();
|
||||
|
||||
while let Some(parent) = tree.parent_id_advanced(current.id(), true) {
|
||||
let parent = dom.get(parent).unwrap();
|
||||
current = parent;
|
||||
let parent_layout = taffy
|
||||
.layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
|
||||
.unwrap();
|
||||
node_layout.location.x += parent_layout.location.x;
|
||||
node_layout.location.y += parent_layout.location.y;
|
||||
}
|
||||
node_layout
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use dioxus_native_core::prelude::*;
|
||||
use dioxus_native_core_macro::partial_derive_state;
|
||||
use shipyard::Component;
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy, Component, Default)]
|
||||
pub(crate) enum PreventDefault {
|
||||
Focus,
|
||||
KeyPress,
|
||||
KeyRelease,
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
MouseDown,
|
||||
Click,
|
||||
MouseEnter,
|
||||
MouseLeave,
|
||||
MouseOut,
|
||||
#[default]
|
||||
Unknown,
|
||||
MouseOver,
|
||||
ContextMenu,
|
||||
Wheel,
|
||||
MouseUp,
|
||||
}
|
||||
|
||||
#[partial_derive_state]
|
||||
impl State for PreventDefault {
|
||||
type ParentDependencies = ();
|
||||
type ChildDependencies = ();
|
||||
type NodeDependencies = ();
|
||||
|
||||
const NODE_MASK: dioxus_native_core::node_ref::NodeMaskBuilder<'static> =
|
||||
dioxus_native_core::node_ref::NodeMaskBuilder::new()
|
||||
.with_attrs(dioxus_native_core::node_ref::AttributeMaskBuilder::Some(&[
|
||||
"dioxus-prevent-default",
|
||||
]))
|
||||
.with_listeners();
|
||||
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
node_view: NodeView,
|
||||
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
_: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: &SendAnyMap,
|
||||
) -> bool {
|
||||
let new = match node_view.attributes().and_then(|mut attrs| {
|
||||
attrs
|
||||
.find(|a| a.attribute.name == "dioxus-prevent-default")
|
||||
.and_then(|a| a.value.as_text())
|
||||
}) {
|
||||
Some("onfocus") => PreventDefault::Focus,
|
||||
Some("onkeypress") => PreventDefault::KeyPress,
|
||||
Some("onkeyrelease") => PreventDefault::KeyRelease,
|
||||
Some("onkeydown") => PreventDefault::KeyDown,
|
||||
Some("onkeyup") => PreventDefault::KeyUp,
|
||||
Some("onclick") => PreventDefault::Click,
|
||||
Some("onmousedown") => PreventDefault::MouseDown,
|
||||
Some("onmouseup") => PreventDefault::MouseUp,
|
||||
Some("onmouseenter") => PreventDefault::MouseEnter,
|
||||
Some("onmouseover") => PreventDefault::MouseOver,
|
||||
Some("onmouseleave") => PreventDefault::MouseLeave,
|
||||
Some("onmouseout") => PreventDefault::MouseOut,
|
||||
Some("onwheel") => PreventDefault::Wheel,
|
||||
Some("oncontextmenu") => PreventDefault::ContextMenu,
|
||||
_ => return false,
|
||||
};
|
||||
if new == *self {
|
||||
false
|
||||
} else {
|
||||
*self = new;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn create<'a>(
|
||||
node_view: NodeView<()>,
|
||||
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
context: &SendAnyMap,
|
||||
) -> Self {
|
||||
let mut myself = Self::default();
|
||||
myself.update(node_view, node, parent, children, context);
|
||||
myself
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard};
|
||||
|
||||
use dioxus_native_core::prelude::*;
|
||||
use shipyard::Unique;
|
||||
use taffy::{
|
||||
geometry::Point,
|
||||
prelude::{Layout, Size},
|
||||
Taffy,
|
||||
};
|
||||
|
||||
use crate::{get_abs_layout, layout_to_screen_space};
|
||||
|
||||
/// Allows querying the layout of nodes after rendering. It will only provide a correct value after a node is rendered.
|
||||
/// Provided as a root context for all tui applictions.
|
||||
/// # Example
|
||||
/// ```rust, ignore
|
||||
/// use dioxus::prelude::*;
|
||||
/// use dioxus_tui::query::Query;
|
||||
/// use dioxus_tui::Size;
|
||||
///
|
||||
/// fn main() {
|
||||
/// dioxus_tui::launch(app);
|
||||
/// }
|
||||
///
|
||||
/// fn app() -> Element {
|
||||
/// let hue = use_signal(|| 0.0);
|
||||
/// let brightness = use_signal(|| 0.0);
|
||||
/// let tui_query: Query = cx.consume_context().unwrap();
|
||||
/// rsx! {
|
||||
/// div{
|
||||
/// width: "100%",
|
||||
/// background_color: "hsl({hue}, 70%, {brightness}%)",
|
||||
/// onmousemove: move |evt| {
|
||||
/// let node = tui_query.get(cx.root_node().mounted_id());
|
||||
/// let Size{width, height} = node.size().unwrap();
|
||||
/// hue.set((evt.data.offset_x as f32/width as f32)*255.0);
|
||||
/// brightness.set((evt.data.offset_y as f32/height as f32)*100.0);
|
||||
/// },
|
||||
/// "hsl({hue}, 70%, {brightness}%)",
|
||||
/// }
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Unique)]
|
||||
pub struct Query {
|
||||
pub(crate) rdom: Arc<RwLock<RealDom>>,
|
||||
pub(crate) stretch: Arc<Mutex<Taffy>>,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn new(rdom: Arc<RwLock<RealDom>>, stretch: Arc<Mutex<Taffy>>) -> Self {
|
||||
Self { rdom, stretch }
|
||||
}
|
||||
|
||||
pub fn get(&self, id: NodeId) -> ElementRef {
|
||||
let rdom = self.rdom.read();
|
||||
let stretch = self.stretch.lock();
|
||||
ElementRef::new(
|
||||
rdom.expect("rdom lock poisoned"),
|
||||
stretch.expect("taffy lock poisoned"),
|
||||
id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ElementRef<'a> {
|
||||
inner: RwLockReadGuard<'a, RealDom>,
|
||||
stretch: MutexGuard<'a, Taffy>,
|
||||
id: NodeId,
|
||||
}
|
||||
|
||||
impl<'a> ElementRef<'a> {
|
||||
pub(crate) fn new(
|
||||
inner: RwLockReadGuard<'a, RealDom>,
|
||||
stretch: MutexGuard<'a, Taffy>,
|
||||
id: NodeId,
|
||||
) -> Self {
|
||||
Self { inner, stretch, id }
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Option<Size<u32>> {
|
||||
self.layout().map(|l| l.size.map(|v| v.round() as u32))
|
||||
}
|
||||
|
||||
pub fn pos(&self) -> Option<Point<u32>> {
|
||||
self.layout().map(|l| Point {
|
||||
x: l.location.x.round() as u32,
|
||||
y: l.location.y.round() as u32,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn layout(&self) -> Option<Layout> {
|
||||
get_layout(self.inner.get(self.id).unwrap(), &self.stretch)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_layout(node: NodeRef, stretch: &Taffy) -> Option<Layout> {
|
||||
let layout = get_abs_layout(node, stretch);
|
||||
let pos = layout.location;
|
||||
|
||||
Some(Layout {
|
||||
order: layout.order,
|
||||
size: layout.size.map(layout_to_screen_space),
|
||||
location: Point {
|
||||
x: layout_to_screen_space(pos.x).round(),
|
||||
y: layout_to_screen_space(pos.y).round(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
use dioxus_native_core::{prelude::*, tree::TreeRef};
|
||||
use ratatui::{layout::Rect, style::Color};
|
||||
use taffy::{
|
||||
geometry::Point,
|
||||
prelude::{Dimension, Layout, Size},
|
||||
Taffy,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
focus::Focused,
|
||||
layout::TaffyLayout,
|
||||
layout_to_screen_space,
|
||||
style::{RinkColor, RinkStyle},
|
||||
style_attributes::{BorderEdge, BorderStyle, StyleModifier},
|
||||
widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
|
||||
Config,
|
||||
};
|
||||
|
||||
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
|
||||
|
||||
pub(crate) fn render_vnode(
|
||||
frame: &mut ratatui::Frame,
|
||||
layout: &Taffy,
|
||||
node: NodeRef,
|
||||
cfg: Config,
|
||||
parent_location: Point<f32>,
|
||||
) {
|
||||
if let NodeType::Placeholder = &*node.node_type() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Layout {
|
||||
mut location, size, ..
|
||||
} = layout
|
||||
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
|
||||
.unwrap();
|
||||
location.x += parent_location.x;
|
||||
location.y += parent_location.y;
|
||||
|
||||
let Point { x: fx, y: fy } = location;
|
||||
let x = layout_to_screen_space(fx).round() as u16;
|
||||
let y = layout_to_screen_space(fy).round() as u16;
|
||||
let Size { width, height } = *size;
|
||||
let width = layout_to_screen_space(fx + width).round() as u16 - x;
|
||||
let height = layout_to_screen_space(fy + height).round() as u16 - y;
|
||||
|
||||
match &*node.node_type() {
|
||||
NodeType::Text(text) => {
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
style: RinkStyle,
|
||||
}
|
||||
|
||||
impl<'a> RinkWidget for Label<'a> {
|
||||
fn render(self, area: Rect, mut buf: RinkBuffer) {
|
||||
for (i, c) in self.text.char_indices() {
|
||||
let mut new_cell = RinkCell::default();
|
||||
new_cell.set_style(self.style);
|
||||
new_cell.symbol = c.to_string();
|
||||
buf.set(area.left() + i as u16, area.top(), new_cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let label = Label {
|
||||
text: &text.text,
|
||||
style: node.get::<StyleModifier>().unwrap().core,
|
||||
};
|
||||
let area = Rect::new(x, y, width, height);
|
||||
|
||||
// the renderer will panic if a node is rendered out of range even if the size is zero
|
||||
if area.width > 0 && area.height > 0 {
|
||||
frame.render_widget(WidgetWithContext::new(label, cfg), area);
|
||||
}
|
||||
}
|
||||
NodeType::Element { .. } => {
|
||||
let area = Rect::new(x, y, width, height);
|
||||
|
||||
// the renderer will panic if a node is rendered out of range even if the size is zero
|
||||
if area.width > 0 && area.height > 0 {
|
||||
frame.render_widget(WidgetWithContext::new(node, cfg), area);
|
||||
}
|
||||
|
||||
let node_id = node.id();
|
||||
let rdom = node.real_dom();
|
||||
for child_id in rdom.tree_ref().children_ids_advanced(node_id, true) {
|
||||
let c = rdom.get(child_id).unwrap();
|
||||
render_vnode(frame, layout, c, cfg, location);
|
||||
}
|
||||
}
|
||||
NodeType::Placeholder => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for NodeRef<'_> {
|
||||
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
|
||||
use ratatui::symbols::line::*;
|
||||
|
||||
enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
fn draw(
|
||||
buf: &mut RinkBuffer,
|
||||
points_history: [[i32; 2]; 3],
|
||||
symbols: &Set,
|
||||
pos: [u16; 2],
|
||||
color: &Option<RinkColor>,
|
||||
) {
|
||||
let [before, current, after] = points_history;
|
||||
let start_dir = match [before[0] - current[0], before[1] - current[1]] {
|
||||
[1, 0] => Direction::Right,
|
||||
[-1, 0] => Direction::Left,
|
||||
[0, 1] => Direction::Down,
|
||||
[0, -1] => Direction::Up,
|
||||
[a, b] => {
|
||||
panic!("draw({before:?} {current:?} {after:?}) {a}, {b} no cell adjacent")
|
||||
}
|
||||
};
|
||||
let end_dir = match [after[0] - current[0], after[1] - current[1]] {
|
||||
[1, 0] => Direction::Right,
|
||||
[-1, 0] => Direction::Left,
|
||||
[0, 1] => Direction::Down,
|
||||
[0, -1] => Direction::Up,
|
||||
[a, b] => {
|
||||
panic!("draw({before:?} {current:?} {after:?}) {a}, {b} no cell adjacent")
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_cell = RinkCell::default();
|
||||
if let Some(c) = color {
|
||||
new_cell.fg = *c;
|
||||
}
|
||||
new_cell.symbol = match [start_dir, end_dir] {
|
||||
[Direction::Down, Direction::Up] => symbols.vertical,
|
||||
[Direction::Down, Direction::Right] => symbols.top_left,
|
||||
[Direction::Down, Direction::Left] => symbols.top_right,
|
||||
[Direction::Up, Direction::Down] => symbols.vertical,
|
||||
[Direction::Up, Direction::Right] => symbols.bottom_left,
|
||||
[Direction::Up, Direction::Left] => symbols.bottom_right,
|
||||
[Direction::Right, Direction::Left] => symbols.horizontal,
|
||||
[Direction::Right, Direction::Up] => symbols.bottom_left,
|
||||
[Direction::Right, Direction::Down] => symbols.top_left,
|
||||
[Direction::Left, Direction::Up] => symbols.bottom_right,
|
||||
[Direction::Left, Direction::Right] => symbols.horizontal,
|
||||
[Direction::Left, Direction::Down] => symbols.top_right,
|
||||
_ => panic!("{before:?} {current:?} {after:?} cannont connect cell to itself"),
|
||||
}
|
||||
.to_string();
|
||||
buf.set(
|
||||
(current[0] + pos[0] as i32) as u16,
|
||||
(current[1] + pos[1] as i32) as u16,
|
||||
new_cell,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_arc(
|
||||
pos: [u16; 2],
|
||||
starting_angle: f32,
|
||||
arc_angle: f32,
|
||||
radius: f32,
|
||||
symbols: &Set,
|
||||
buf: &mut RinkBuffer,
|
||||
color: &Option<RinkColor>,
|
||||
) {
|
||||
if radius < 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let num_points = (radius * arc_angle) as i32;
|
||||
let starting_point = [
|
||||
(starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32,
|
||||
(starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32,
|
||||
];
|
||||
// keep track of the last 3 point to allow filling diagonals
|
||||
let mut points_history = [
|
||||
[0, 0],
|
||||
{
|
||||
// change the x or y value based on which one is changing quicker
|
||||
let ddx = -starting_angle.sin();
|
||||
let ddy = starting_angle.cos();
|
||||
if ddx.abs() > ddy.abs() {
|
||||
[starting_point[0] - ddx.signum() as i32, starting_point[1]]
|
||||
} else {
|
||||
[starting_point[0], starting_point[1] - ddy.signum() as i32]
|
||||
}
|
||||
},
|
||||
starting_point,
|
||||
];
|
||||
|
||||
for i in 1..=num_points {
|
||||
let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle;
|
||||
let x = angle.cos() * radius * RADIUS_MULTIPLIER[0];
|
||||
let y = angle.sin() * radius * RADIUS_MULTIPLIER[1];
|
||||
let new = [x as i32, y as i32];
|
||||
|
||||
if new != points_history[2] {
|
||||
points_history = [points_history[1], points_history[2], new];
|
||||
|
||||
let dx = points_history[2][0] - points_history[1][0];
|
||||
let dy = points_history[2][1] - points_history[1][1];
|
||||
// fill diagonals
|
||||
if dx != 0 && dy != 0 {
|
||||
let connecting_point = match [dx, dy] {
|
||||
[1, 1] => [points_history[1][0] + 1, points_history[1][1]],
|
||||
[1, -1] => [points_history[1][0], points_history[1][1] - 1],
|
||||
[-1, 1] => [points_history[1][0], points_history[1][1] + 1],
|
||||
[-1, -1] => [points_history[1][0] - 1, points_history[1][1]],
|
||||
_ => todo!(),
|
||||
};
|
||||
draw(
|
||||
buf,
|
||||
[points_history[0], points_history[1], connecting_point],
|
||||
symbols,
|
||||
pos,
|
||||
color,
|
||||
);
|
||||
points_history = [points_history[1], connecting_point, points_history[2]];
|
||||
}
|
||||
|
||||
draw(buf, points_history, symbols, pos, color);
|
||||
}
|
||||
}
|
||||
|
||||
points_history = [points_history[1], points_history[2], {
|
||||
// change the x or y value based on which one is changing quicker
|
||||
let ddx = -(starting_angle + arc_angle).sin();
|
||||
let ddy = (starting_angle + arc_angle).cos();
|
||||
if ddx.abs() > ddy.abs() {
|
||||
[
|
||||
points_history[2][0] + ddx.signum() as i32,
|
||||
points_history[2][1],
|
||||
]
|
||||
} else {
|
||||
[
|
||||
points_history[2][0],
|
||||
points_history[2][1] + ddy.signum() as i32,
|
||||
]
|
||||
}
|
||||
}];
|
||||
|
||||
draw(buf, points_history, symbols, pos, color);
|
||||
}
|
||||
|
||||
fn get_radius(border: &BorderEdge, area: Rect) -> f32 {
|
||||
match border.style {
|
||||
BorderStyle::Hidden => 0.0,
|
||||
BorderStyle::None => 0.0,
|
||||
_ => match border.radius {
|
||||
Dimension::Percent(p) => p * area.width as f32 / 100.0,
|
||||
Dimension::Points(p) => p,
|
||||
_ => todo!(),
|
||||
}
|
||||
.abs()
|
||||
.min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0)
|
||||
.min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0),
|
||||
}
|
||||
}
|
||||
|
||||
if area.area() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: only render inside borders
|
||||
for x in area.left()..area.right() {
|
||||
for y in area.top()..area.bottom() {
|
||||
let mut new_cell = RinkCell::default();
|
||||
if let Some(c) = self.get::<StyleModifier>().unwrap().core.bg {
|
||||
new_cell.bg = c;
|
||||
}
|
||||
if let Some(focused) = self.get::<Focused>() {
|
||||
if focused.0 {
|
||||
new_cell.bg.alpha = 100;
|
||||
new_cell.bg.color = new_cell.bg.blend(Color::White);
|
||||
}
|
||||
}
|
||||
buf.set(x, y, new_cell);
|
||||
}
|
||||
}
|
||||
|
||||
let style = self.get::<StyleModifier>().unwrap();
|
||||
|
||||
let borders = &style.modifier.borders;
|
||||
|
||||
let last_edge = &borders.left;
|
||||
let current_edge = &borders.top;
|
||||
if let Some(symbols) = current_edge.style.symbol_set() {
|
||||
// the radius for the curve between this line and the next
|
||||
let r = get_radius(current_edge, area);
|
||||
let radius = [
|
||||
(r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
// the radius for the curve between this line and the last
|
||||
let last_r = get_radius(last_edge, area);
|
||||
let last_radius = [
|
||||
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
let color = current_edge.color.or(style.core.fg);
|
||||
let mut new_cell = RinkCell::default();
|
||||
if let Some(c) = color {
|
||||
new_cell.fg = c;
|
||||
}
|
||||
for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) {
|
||||
new_cell.symbol = symbols.horizontal.to_string();
|
||||
buf.set(x, area.top(), new_cell.clone());
|
||||
}
|
||||
draw_arc(
|
||||
[area.right() - radius[0] - 1, area.top() + radius[1]],
|
||||
std::f32::consts::FRAC_PI_2 * 3.0,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
r,
|
||||
&symbols,
|
||||
&mut buf,
|
||||
&color,
|
||||
);
|
||||
}
|
||||
|
||||
let last_edge = &borders.top;
|
||||
let current_edge = &borders.right;
|
||||
if let Some(symbols) = current_edge.style.symbol_set() {
|
||||
// the radius for the curve between this line and the next
|
||||
let r = get_radius(current_edge, area);
|
||||
let radius = [
|
||||
(r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
// the radius for the curve between this line and the last
|
||||
let last_r = get_radius(last_edge, area);
|
||||
let last_radius = [
|
||||
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
let color = current_edge.color.or(style.core.fg);
|
||||
let mut new_cell = RinkCell::default();
|
||||
if let Some(c) = color {
|
||||
new_cell.fg = c;
|
||||
}
|
||||
for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) {
|
||||
new_cell.symbol = symbols.vertical.to_string();
|
||||
buf.set(area.right() - 1, y, new_cell.clone());
|
||||
}
|
||||
draw_arc(
|
||||
[area.right() - radius[0] - 1, area.bottom() - radius[1] - 1],
|
||||
0.0,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
r,
|
||||
&symbols,
|
||||
&mut buf,
|
||||
&color,
|
||||
);
|
||||
}
|
||||
|
||||
let last_edge = &borders.right;
|
||||
let current_edge = &borders.bottom;
|
||||
if let Some(symbols) = current_edge.style.symbol_set() {
|
||||
// the radius for the curve between this line and the next
|
||||
let r = get_radius(current_edge, area);
|
||||
let radius = [
|
||||
(r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
// the radius for the curve between this line and the last
|
||||
let last_r = get_radius(last_edge, area);
|
||||
let last_radius = [
|
||||
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
let color = current_edge.color.or(style.core.fg);
|
||||
let mut new_cell = RinkCell::default();
|
||||
if let Some(c) = color {
|
||||
new_cell.fg = c;
|
||||
}
|
||||
for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) {
|
||||
new_cell.symbol = symbols.horizontal.to_string();
|
||||
buf.set(x, area.bottom() - 1, new_cell.clone());
|
||||
}
|
||||
draw_arc(
|
||||
[area.left() + radius[0], area.bottom() - radius[1] - 1],
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
r,
|
||||
&symbols,
|
||||
&mut buf,
|
||||
&color,
|
||||
);
|
||||
}
|
||||
|
||||
let last_edge = &borders.bottom;
|
||||
let current_edge = &borders.left;
|
||||
if let Some(symbols) = current_edge.style.symbol_set() {
|
||||
// the radius for the curve between this line and the next
|
||||
let r = get_radius(current_edge, area);
|
||||
let radius = [
|
||||
(r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
// the radius for the curve between this line and the last
|
||||
let last_r = get_radius(last_edge, area);
|
||||
let last_radius = [
|
||||
(last_r * RADIUS_MULTIPLIER[0]) as u16,
|
||||
(last_r * RADIUS_MULTIPLIER[1]) as u16,
|
||||
];
|
||||
let color = current_edge.color.or(style.core.fg);
|
||||
let mut new_cell = RinkCell::default();
|
||||
if let Some(c) = color {
|
||||
new_cell.fg = c;
|
||||
}
|
||||
for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) {
|
||||
new_cell.symbol = symbols.vertical.to_string();
|
||||
buf.set(area.left(), y, new_cell.clone());
|
||||
}
|
||||
draw_arc(
|
||||
[area.left() + radius[0], area.top() + radius[1]],
|
||||
std::f32::consts::PI,
|
||||
std::f32::consts::FRAC_PI_2,
|
||||
r,
|
||||
&symbols,
|
||||
&mut buf,
|
||||
&color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
use std::{num::ParseFloatError, str::FromStr};
|
||||
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
use crate::RenderingMode;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct RinkColor {
|
||||
pub color: Color,
|
||||
pub alpha: u8,
|
||||
}
|
||||
|
||||
impl Default for RinkColor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::Black,
|
||||
alpha: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkColor {
|
||||
pub fn blend(self, other: Color) -> Color {
|
||||
if self.color == Color::Reset {
|
||||
Color::Reset
|
||||
} else if self.alpha == 0 {
|
||||
other
|
||||
} else {
|
||||
let [sr, sg, sb] = to_rgb(self.color).map(|e| e as u16);
|
||||
let [or, og, ob] = to_rgb(other).map(|e| e as u16);
|
||||
let sa = self.alpha as u16;
|
||||
let rsa = 255 - sa;
|
||||
Color::Rgb(
|
||||
((sr * sa + or * rsa) / 255) as u8,
|
||||
((sg * sa + og * rsa) / 255) as u8,
|
||||
((sb * sa + ob * rsa) / 255) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(
|
||||
v: &str,
|
||||
current_max_output: f32,
|
||||
required_max_output: f32,
|
||||
) -> Result<f32, ParseFloatError> {
|
||||
if let Some(stripped) = v.strip_suffix('%') {
|
||||
Ok((stripped.trim().parse::<f32>()? / 100.0) * required_max_output)
|
||||
} else {
|
||||
Ok((v.trim().parse::<f32>()? / current_max_output) * required_max_output)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ParseColorError;
|
||||
|
||||
fn parse_hex(color: &str) -> Result<Color, ParseColorError> {
|
||||
let mut values = [0, 0, 0];
|
||||
let mut color_ok = true;
|
||||
for i in 0..values.len() {
|
||||
if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) {
|
||||
values[i] = v;
|
||||
} else {
|
||||
color_ok = false;
|
||||
}
|
||||
}
|
||||
if color_ok {
|
||||
Ok(Color::Rgb(values[0], values[1], values[2]))
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rgb(color: &str) -> Result<Color, ParseColorError> {
|
||||
let mut values = [0, 0, 0];
|
||||
let mut color_ok = true;
|
||||
for (v, i) in color.split(',').zip(0..values.len()) {
|
||||
if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) {
|
||||
values[i] = v as u8;
|
||||
} else {
|
||||
color_ok = false;
|
||||
}
|
||||
}
|
||||
if color_ok {
|
||||
Ok(Color::Rgb(values[0], values[1], values[2]))
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hsl(color: &str) -> Result<Color, ParseColorError> {
|
||||
let mut values = [0.0, 0.0, 0.0];
|
||||
let mut color_ok = true;
|
||||
for (v, i) in color.split(',').zip(0..values.len()) {
|
||||
if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) {
|
||||
values[i] = v;
|
||||
} else {
|
||||
color_ok = false;
|
||||
}
|
||||
}
|
||||
if color_ok {
|
||||
let [h, s, l] = values;
|
||||
let rgb = if s == 0.0 {
|
||||
[l as u8; 3]
|
||||
} else {
|
||||
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
|
||||
if t < 0.0 {
|
||||
t += 1.0;
|
||||
}
|
||||
if t > 1.0 {
|
||||
t -= 1.0;
|
||||
}
|
||||
if t < 1.0 / 6.0 {
|
||||
p + (q - p) * 6.0 * t
|
||||
} else if t < 1.0 / 2.0 {
|
||||
q
|
||||
} else if t < 2.0 / 3.0 {
|
||||
p + (q - p) * (2.0 / 3.0 - t) * 6.0
|
||||
} else {
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
let q = if l < 0.5 {
|
||||
l * (1.0 + s)
|
||||
} else {
|
||||
l + s - l * s
|
||||
};
|
||||
let p = 2.0 * l - q;
|
||||
[
|
||||
(hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8,
|
||||
(hue_to_rgb(p, q, h) * 255.0) as u8,
|
||||
(hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8,
|
||||
]
|
||||
};
|
||||
|
||||
Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RinkColor {
|
||||
type Err = ParseColorError;
|
||||
|
||||
fn from_str(color: &str) -> Result<Self, Self::Err> {
|
||||
match color {
|
||||
"red" => Ok(RinkColor {
|
||||
color: Color::Red,
|
||||
alpha: 255,
|
||||
}),
|
||||
"black" => Ok(RinkColor {
|
||||
color: Color::Black,
|
||||
alpha: 255,
|
||||
}),
|
||||
"green" => Ok(RinkColor {
|
||||
color: Color::Green,
|
||||
alpha: 255,
|
||||
}),
|
||||
"yellow" => Ok(RinkColor {
|
||||
color: Color::Yellow,
|
||||
alpha: 255,
|
||||
}),
|
||||
"blue" => Ok(RinkColor {
|
||||
color: Color::Blue,
|
||||
alpha: 255,
|
||||
}),
|
||||
"magenta" => Ok(RinkColor {
|
||||
color: Color::Magenta,
|
||||
alpha: 255,
|
||||
}),
|
||||
"cyan" => Ok(RinkColor {
|
||||
color: Color::Cyan,
|
||||
alpha: 255,
|
||||
}),
|
||||
"gray" => Ok(RinkColor {
|
||||
color: Color::Gray,
|
||||
alpha: 255,
|
||||
}),
|
||||
"darkgray" => Ok(RinkColor {
|
||||
color: Color::DarkGray,
|
||||
alpha: 255,
|
||||
}),
|
||||
// light red does not exist
|
||||
"orangered" => Ok(RinkColor {
|
||||
color: Color::LightRed,
|
||||
alpha: 255,
|
||||
}),
|
||||
"lightgreen" => Ok(RinkColor {
|
||||
color: Color::LightGreen,
|
||||
alpha: 255,
|
||||
}),
|
||||
"lightyellow" => Ok(RinkColor {
|
||||
color: Color::LightYellow,
|
||||
alpha: 255,
|
||||
}),
|
||||
"lightblue" => Ok(RinkColor {
|
||||
color: Color::LightBlue,
|
||||
alpha: 255,
|
||||
}),
|
||||
// light magenta does not exist
|
||||
"orchid" => Ok(RinkColor {
|
||||
color: Color::LightMagenta,
|
||||
alpha: 255,
|
||||
}),
|
||||
"lightcyan" => Ok(RinkColor {
|
||||
color: Color::LightCyan,
|
||||
alpha: 255,
|
||||
}),
|
||||
"white" => Ok(RinkColor {
|
||||
color: Color::White,
|
||||
alpha: 255,
|
||||
}),
|
||||
_ => {
|
||||
if color.len() == 7 && color.starts_with('#') {
|
||||
parse_hex(color).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: 255,
|
||||
})
|
||||
} else if let Some(stripped) = color.strip_prefix("rgb(") {
|
||||
let color_values = stripped.trim_end_matches(')');
|
||||
if color.matches(',').count() == 3 {
|
||||
let (alpha, rgb_values) =
|
||||
color_values.rsplit_once(',').ok_or(ParseColorError)?;
|
||||
if let Ok(a) = alpha.parse() {
|
||||
parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a })
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
} else {
|
||||
parse_rgb(color_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: 255,
|
||||
})
|
||||
}
|
||||
} else if let Some(stripped) = color.strip_prefix("rgba(") {
|
||||
let color_values = stripped.trim_end_matches(')');
|
||||
if color.matches(',').count() == 3 {
|
||||
let (rgb_values, alpha) =
|
||||
color_values.rsplit_once(',').ok_or(ParseColorError)?;
|
||||
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
|
||||
parse_rgb(rgb_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: (a * 255.0) as u8,
|
||||
})
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
} else {
|
||||
parse_rgb(color_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: 255,
|
||||
})
|
||||
}
|
||||
} else if let Some(stripped) = color.strip_prefix("hsl(") {
|
||||
let color_values = stripped.trim_end_matches(')');
|
||||
if color.matches(',').count() == 3 {
|
||||
let (rgb_values, alpha) =
|
||||
color_values.rsplit_once(',').ok_or(ParseColorError)?;
|
||||
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
|
||||
parse_hsl(rgb_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: (a * 255.0) as u8,
|
||||
})
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
} else {
|
||||
parse_hsl(color_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: 255,
|
||||
})
|
||||
}
|
||||
} else if let Some(stripped) = color.strip_prefix("hsla(") {
|
||||
let color_values = stripped.trim_end_matches(')');
|
||||
if color.matches(',').count() == 3 {
|
||||
let (rgb_values, alpha) =
|
||||
color_values.rsplit_once(',').ok_or(ParseColorError)?;
|
||||
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
|
||||
parse_hsl(rgb_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: (a * 255.0) as u8,
|
||||
})
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
} else {
|
||||
parse_hsl(color_values).map(|c| RinkColor {
|
||||
color: c,
|
||||
alpha: 255,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(ParseColorError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_rgb(c: Color) -> [u8; 3] {
|
||||
match c {
|
||||
Color::Black => [0, 0, 0],
|
||||
Color::Red => [255, 0, 0],
|
||||
Color::Green => [0, 128, 0],
|
||||
Color::Yellow => [255, 255, 0],
|
||||
Color::Blue => [0, 0, 255],
|
||||
Color::Magenta => [255, 0, 255],
|
||||
Color::Cyan => [0, 255, 255],
|
||||
Color::Gray => [128, 128, 128],
|
||||
Color::DarkGray => [169, 169, 169],
|
||||
Color::LightRed => [255, 69, 0],
|
||||
Color::LightGreen => [144, 238, 144],
|
||||
Color::LightYellow => [255, 255, 224],
|
||||
Color::LightBlue => [173, 216, 230],
|
||||
Color::LightMagenta => [218, 112, 214],
|
||||
Color::LightCyan => [224, 255, 255],
|
||||
Color::White => [255, 255, 255],
|
||||
Color::Rgb(r, g, b) => [r, g, b],
|
||||
Color::Indexed(idx) => match idx {
|
||||
16..=231 => {
|
||||
let v = idx - 16;
|
||||
// add 3 to round up
|
||||
let r = ((v as u16 / 36) * 255 + 3) / 5;
|
||||
let g = (((v as u16 % 36) / 6) * 255 + 3) / 5;
|
||||
let b = ((v as u16 % 6) * 255 + 3) / 5;
|
||||
[r as u8, g as u8, b as u8]
|
||||
}
|
||||
232..=255 => {
|
||||
let l = (idx - 232) / 24;
|
||||
[l; 3]
|
||||
}
|
||||
// plasmo will never generate these colors, but they might be on the screen from another program
|
||||
_ => [0, 0, 0],
|
||||
},
|
||||
Color::Reset => [0, 0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert(mode: RenderingMode, c: Color) -> Color {
|
||||
if let Color::Reset = c {
|
||||
c
|
||||
} else {
|
||||
match mode {
|
||||
crate::RenderingMode::BaseColors => match c {
|
||||
Color::Rgb(_, _, _) => panic!("cannot convert rgb color to base color"),
|
||||
Color::Indexed(_) => panic!("cannot convert Ansi color to base color"),
|
||||
_ => c,
|
||||
},
|
||||
crate::RenderingMode::Rgb => {
|
||||
let rgb = to_rgb(c);
|
||||
Color::Rgb(rgb[0], rgb[1], rgb[2])
|
||||
}
|
||||
crate::RenderingMode::Ansi => match c {
|
||||
Color::Indexed(_) => c,
|
||||
_ => {
|
||||
let rgb = to_rgb(c);
|
||||
// 16-231: 6 × 6 × 6 color cube
|
||||
// 232-255: 23 step grayscale
|
||||
if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
|
||||
let idx = 232 + (rgb[0] as u16 * 23 / 255) as u8;
|
||||
Color::Indexed(idx)
|
||||
} else {
|
||||
let r = (rgb[0] as u16 * 5) / 255;
|
||||
let g = (rgb[1] as u16 * 5) / 255;
|
||||
let b = (rgb[2] as u16 * 5) / 255;
|
||||
let idx = 16 + r * 36 + g * 6 + b;
|
||||
Color::Indexed(idx as u8)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_to_ansi() {
|
||||
for idx in 17..=231 {
|
||||
let idxed = Color::Indexed(idx);
|
||||
let rgb = to_rgb(idxed);
|
||||
// gray scale colors have two equivelent repersentations
|
||||
let color = Color::Rgb(rgb[0], rgb[1], rgb[2]);
|
||||
let converted = convert(RenderingMode::Ansi, color);
|
||||
if let Color::Indexed(i) = converted {
|
||||
if rgb[0] != rgb[1] || rgb[1] != rgb[2] {
|
||||
assert_eq!(idxed, converted);
|
||||
} else {
|
||||
assert!(i >= 232);
|
||||
}
|
||||
} else {
|
||||
panic!("color is not indexed")
|
||||
}
|
||||
}
|
||||
for idx in 232..=255 {
|
||||
let idxed = Color::Indexed(idx);
|
||||
let rgb = to_rgb(idxed);
|
||||
assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub struct RinkStyle {
|
||||
pub fg: Option<RinkColor>,
|
||||
pub bg: Option<RinkColor>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for RinkStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fg: Some(RinkColor {
|
||||
color: Color::White,
|
||||
alpha: 255,
|
||||
}),
|
||||
bg: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkStyle {
|
||||
pub fn add_modifier(mut self, m: Modifier) -> Self {
|
||||
self.sub_modifier.remove(m);
|
||||
self.add_modifier.insert(m);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn remove_modifier(mut self, m: Modifier) -> Self {
|
||||
self.add_modifier.remove(m);
|
||||
self.sub_modifier.insert(m);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn merge(mut self, other: RinkStyle) -> Self {
|
||||
self.fg = self.fg.or(other.fg);
|
||||
self.add_modifier(other.add_modifier)
|
||||
.remove_modifier(other.sub_modifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RinkStyle> for Style {
|
||||
fn from(val: RinkStyle) -> Self {
|
||||
Style {
|
||||
underline_color: None,
|
||||
fg: val.fg.map(|c| c.color),
|
||||
bg: val.bg.map(|c| c.color),
|
||||
add_modifier: val.add_modifier,
|
||||
sub_modifier: val.sub_modifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
/*
|
||||
- [ ] pub display: Display,
|
||||
- [x] pub position_type: PositionType, --> kinda, taffy doesnt support everything
|
||||
- [ ] pub direction: Direction,
|
||||
|
||||
- [x] pub flex_direction: FlexDirection,
|
||||
- [x] pub flex_wrap: FlexWrap,
|
||||
- [x] pub flex_grow: f32,
|
||||
- [x] pub flex_shrink: f32,
|
||||
- [x] pub flex_basis: Dimension,
|
||||
|
||||
- [x] pub overflow: Overflow, ---> kinda implemented... taffy doesnt have support for directional overflow
|
||||
|
||||
- [x] pub align_items: AlignItems,
|
||||
- [x] pub align_self: AlignSelf,
|
||||
- [x] pub align_content: AlignContent,
|
||||
|
||||
- [x] pub margin: Rect<Dimension>,
|
||||
- [x] pub padding: Rect<Dimension>,
|
||||
|
||||
- [x] pub justify_content: JustifyContent,
|
||||
- [ ] pub position: Rect<Dimension>,
|
||||
- [x] pub border: Rect<Dimension>,
|
||||
|
||||
- [ ] pub size: Size<Dimension>, ----> ??? seems to only be relevant for input?
|
||||
- [ ] pub min_size: Size<Dimension>,
|
||||
- [ ] pub max_size: Size<Dimension>,
|
||||
|
||||
- [ ] pub aspect_ratio: Number,
|
||||
*/
|
||||
|
||||
use dioxus_native_core::{
|
||||
layout_attributes::parse_value,
|
||||
node::OwnedAttributeView,
|
||||
node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView},
|
||||
prelude::*,
|
||||
};
|
||||
use dioxus_native_core_macro::partial_derive_state;
|
||||
use shipyard::Component;
|
||||
use taffy::prelude::*;
|
||||
|
||||
use crate::style::{RinkColor, RinkStyle};
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Debug, Component)]
|
||||
pub struct StyleModifier {
|
||||
pub core: RinkStyle,
|
||||
pub modifier: TuiModifier,
|
||||
}
|
||||
|
||||
#[partial_derive_state]
|
||||
impl State for StyleModifier {
|
||||
type ParentDependencies = (Self,);
|
||||
type ChildDependencies = ();
|
||||
type NodeDependencies = ();
|
||||
|
||||
// todo: seperate each attribute into it's own class
|
||||
const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new()
|
||||
.with_attrs(AttributeMaskBuilder::Some(SORTED_STYLE_ATTRS))
|
||||
.with_element();
|
||||
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
node_view: NodeView,
|
||||
_: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
_: &SendAnyMap,
|
||||
) -> bool {
|
||||
let mut new = StyleModifier::default();
|
||||
if parent.is_some() {
|
||||
new.core.fg = None;
|
||||
}
|
||||
|
||||
// handle text modifier elements
|
||||
if node_view.namespace().is_none() {
|
||||
if let Some(tag) = node_view.tag() {
|
||||
match tag {
|
||||
"b" => apply_style_attributes("font-weight", "bold", &mut new),
|
||||
"strong" => apply_style_attributes("font-weight", "bold", &mut new),
|
||||
"u" => apply_style_attributes("text-decoration", "underline", &mut new),
|
||||
"ins" => apply_style_attributes("text-decoration", "underline", &mut new),
|
||||
"del" => apply_style_attributes("text-decoration", "line-through", &mut new),
|
||||
"i" => apply_style_attributes("font-style", "italic", &mut new),
|
||||
"em" => apply_style_attributes("font-style", "italic", &mut new),
|
||||
"mark" => {
|
||||
apply_style_attributes("background-color", "rgba(241, 231, 64, 50%)", self)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gather up all the styles from the attribute list
|
||||
if let Some(attrs) = node_view.attributes() {
|
||||
for OwnedAttributeView {
|
||||
attribute, value, ..
|
||||
} in attrs
|
||||
{
|
||||
if let Some(text) = value.as_text() {
|
||||
apply_style_attributes(&attribute.name, text, &mut new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep the text styling from the parent element
|
||||
if let Some((parent,)) = parent {
|
||||
let mut new_style = new.core.merge(parent.core);
|
||||
new_style.bg = new.core.bg;
|
||||
new.core = new_style;
|
||||
}
|
||||
if &mut new != self {
|
||||
*self = new;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn create<'a>(
|
||||
node_view: NodeView<()>,
|
||||
node: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
|
||||
parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
children: Vec<<Self::ChildDependencies as Dependancy>::ElementBorrowed<'a>>,
|
||||
context: &SendAnyMap,
|
||||
) -> Self {
|
||||
let mut myself = Self::default();
|
||||
myself.update(node_view, node, parent, children, context);
|
||||
myself
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Debug)]
|
||||
pub struct TuiModifier {
|
||||
pub borders: Borders,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Debug)]
|
||||
pub struct Borders {
|
||||
pub top: BorderEdge,
|
||||
pub right: BorderEdge,
|
||||
pub bottom: BorderEdge,
|
||||
pub left: BorderEdge,
|
||||
}
|
||||
|
||||
impl Borders {
|
||||
fn slice(&mut self) -> [&mut BorderEdge; 4] {
|
||||
[
|
||||
&mut self.top,
|
||||
&mut self.right,
|
||||
&mut self.bottom,
|
||||
&mut self.left,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct BorderEdge {
|
||||
pub color: Option<RinkColor>,
|
||||
pub style: BorderStyle,
|
||||
pub width: Dimension,
|
||||
pub radius: Dimension,
|
||||
}
|
||||
|
||||
impl Default for BorderEdge {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: None,
|
||||
style: BorderStyle::None,
|
||||
width: Dimension::Points(0.0),
|
||||
radius: Dimension::Points(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum BorderStyle {
|
||||
Dotted,
|
||||
Dashed,
|
||||
Solid,
|
||||
Double,
|
||||
Groove,
|
||||
Ridge,
|
||||
Inset,
|
||||
Outset,
|
||||
Hidden,
|
||||
None,
|
||||
}
|
||||
|
||||
impl BorderStyle {
|
||||
pub fn symbol_set(&self) -> Option<ratatui::symbols::line::Set> {
|
||||
use ratatui::symbols::line::*;
|
||||
const DASHED: Set = Set {
|
||||
horizontal: "╌",
|
||||
vertical: "╎",
|
||||
..NORMAL
|
||||
};
|
||||
const DOTTED: Set = Set {
|
||||
horizontal: "┈",
|
||||
vertical: "┊",
|
||||
..NORMAL
|
||||
};
|
||||
match self {
|
||||
BorderStyle::Dotted => Some(DOTTED),
|
||||
BorderStyle::Dashed => Some(DASHED),
|
||||
BorderStyle::Solid => Some(NORMAL),
|
||||
BorderStyle::Double => Some(DOUBLE),
|
||||
BorderStyle::Groove => Some(NORMAL),
|
||||
BorderStyle::Ridge => Some(NORMAL),
|
||||
BorderStyle::Inset => Some(NORMAL),
|
||||
BorderStyle::Outset => Some(NORMAL),
|
||||
BorderStyle::Hidden => None,
|
||||
BorderStyle::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// applies the entire html namespace defined in dioxus-html
|
||||
pub fn apply_style_attributes(
|
||||
//
|
||||
name: &str,
|
||||
value: &str,
|
||||
style: &mut StyleModifier,
|
||||
) {
|
||||
match name {
|
||||
"animation"
|
||||
| "animation-delay"
|
||||
| "animation-direction"
|
||||
| "animation-duration"
|
||||
| "animation-fill-mode"
|
||||
| "animation-iteration-count"
|
||||
| "animation-name"
|
||||
| "animation-play-state"
|
||||
| "animation-timing-function" => apply_animation(name, value, style),
|
||||
|
||||
"backface-visibility" => {}
|
||||
|
||||
"background"
|
||||
| "background-attachment"
|
||||
| "background-clip"
|
||||
| "background-color"
|
||||
| "background-image"
|
||||
| "background-origin"
|
||||
| "background-position"
|
||||
| "background-repeat"
|
||||
| "background-size" => apply_background(name, value, style),
|
||||
|
||||
"border"
|
||||
| "border-bottom"
|
||||
| "border-bottom-color"
|
||||
| "border-bottom-left-radius"
|
||||
| "border-bottom-right-radius"
|
||||
| "border-bottom-style"
|
||||
| "border-bottom-width"
|
||||
| "border-collapse"
|
||||
| "border-color"
|
||||
| "border-image"
|
||||
| "border-image-outset"
|
||||
| "border-image-repeat"
|
||||
| "border-image-slice"
|
||||
| "border-image-source"
|
||||
| "border-image-width"
|
||||
| "border-left"
|
||||
| "border-left-color"
|
||||
| "border-left-style"
|
||||
| "border-left-width"
|
||||
| "border-radius"
|
||||
| "border-right"
|
||||
| "border-right-color"
|
||||
| "border-right-style"
|
||||
| "border-right-width"
|
||||
| "border-spacing"
|
||||
| "border-style"
|
||||
| "border-top"
|
||||
| "border-top-color"
|
||||
| "border-top-left-radius"
|
||||
| "border-top-right-radius"
|
||||
| "border-top-style"
|
||||
| "border-top-width"
|
||||
| "border-width" => apply_border(name, value, style),
|
||||
|
||||
"bottom" => {}
|
||||
"box-shadow" => {}
|
||||
"box-sizing" => {}
|
||||
"caption-side" => {}
|
||||
"clear" => {}
|
||||
"clip" => {}
|
||||
|
||||
"color" => {
|
||||
if let Ok(c) = value.parse() {
|
||||
style.core.fg.replace(c);
|
||||
}
|
||||
}
|
||||
|
||||
"columns" => {}
|
||||
|
||||
"content" => {}
|
||||
"counter-increment" => {}
|
||||
"counter-reset" => {}
|
||||
|
||||
"cursor" => {}
|
||||
|
||||
"empty-cells" => {}
|
||||
|
||||
"float" => {}
|
||||
|
||||
"font" | "font-family" | "font-size" | "font-size-adjust" | "font-stretch"
|
||||
| "font-style" | "font-variant" | "font-weight" => apply_font(name, value, style),
|
||||
|
||||
"letter-spacing" => {}
|
||||
"line-height" => {}
|
||||
|
||||
"list-style" | "list-style-image" | "list-style-position" | "list-style-type" => {}
|
||||
|
||||
"opacity" => {}
|
||||
"order" => {}
|
||||
"outline" => {}
|
||||
|
||||
"outline-color" | "outline-offset" | "outline-style" | "outline-width" => {}
|
||||
|
||||
"page-break-after" | "page-break-before" | "page-break-inside" => {}
|
||||
|
||||
"perspective" | "perspective-origin" => {}
|
||||
|
||||
"pointer-events" => {}
|
||||
|
||||
"quotes" => {}
|
||||
"resize" => {}
|
||||
"tab-size" => {}
|
||||
"table-layout" => {}
|
||||
|
||||
"text-align"
|
||||
| "text-align-last"
|
||||
| "text-decoration"
|
||||
| "text-decoration-color"
|
||||
| "text-decoration-line"
|
||||
| "text-decoration-style"
|
||||
| "text-indent"
|
||||
| "text-justify"
|
||||
| "text-overflow"
|
||||
| "text-shadow"
|
||||
| "text-transform" => apply_text(name, value, style),
|
||||
|
||||
"transition"
|
||||
| "transition-delay"
|
||||
| "transition-duration"
|
||||
| "transition-property"
|
||||
| "transition-timing-function" => apply_transition(name, value, style),
|
||||
|
||||
"visibility" => {}
|
||||
"white-space" => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_background(name: &str, value: &str, style: &mut StyleModifier) {
|
||||
match name {
|
||||
"background-color" => {
|
||||
if let Ok(c) = value.parse() {
|
||||
style.core.bg.replace(c);
|
||||
}
|
||||
}
|
||||
"background" => {}
|
||||
"background-attachment" => {}
|
||||
"background-clip" => {}
|
||||
"background-image" => {}
|
||||
"background-origin" => {}
|
||||
"background-position" => {}
|
||||
"background-repeat" => {}
|
||||
"background-size" => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_border(name: &str, value: &str, style: &mut StyleModifier) {
|
||||
fn parse_border_style(v: &str) -> BorderStyle {
|
||||
match v {
|
||||
"dotted" => BorderStyle::Dotted,
|
||||
"dashed" => BorderStyle::Dashed,
|
||||
"solid" => BorderStyle::Solid,
|
||||
"double" => BorderStyle::Double,
|
||||
"groove" => BorderStyle::Groove,
|
||||
"ridge" => BorderStyle::Ridge,
|
||||
"inset" => BorderStyle::Inset,
|
||||
"outset" => BorderStyle::Outset,
|
||||
"none" => BorderStyle::None,
|
||||
"hidden" => BorderStyle::Hidden,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
match name {
|
||||
"border" => {}
|
||||
"border-bottom" => {}
|
||||
"border-bottom-color" => {
|
||||
if let Ok(c) = value.parse() {
|
||||
style.modifier.borders.bottom.color = Some(c);
|
||||
}
|
||||
}
|
||||
"border-bottom-left-radius" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.left.radius = v;
|
||||
}
|
||||
}
|
||||
"border-bottom-right-radius" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.right.radius = v;
|
||||
}
|
||||
}
|
||||
"border-bottom-style" => style.modifier.borders.bottom.style = parse_border_style(value),
|
||||
"border-bottom-width" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.bottom.width = v;
|
||||
}
|
||||
}
|
||||
"border-collapse" => {}
|
||||
"border-color" => {
|
||||
let values: Vec<_> = value.split(' ').collect();
|
||||
if values.len() == 1 {
|
||||
if let Ok(c) = values[0].parse() {
|
||||
style
|
||||
.modifier
|
||||
.borders
|
||||
.slice()
|
||||
.iter_mut()
|
||||
.for_each(|b| b.color = Some(c));
|
||||
}
|
||||
} else {
|
||||
for (v, b) in values
|
||||
.into_iter()
|
||||
.zip(style.modifier.borders.slice().iter_mut())
|
||||
{
|
||||
if let Ok(c) = v.parse() {
|
||||
b.color = Some(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"border-image" => {}
|
||||
"border-image-outset" => {}
|
||||
"border-image-repeat" => {}
|
||||
"border-image-slice" => {}
|
||||
"border-image-source" => {}
|
||||
"border-image-width" => {}
|
||||
"border-left" => {}
|
||||
"border-left-color" => {
|
||||
if let Ok(c) = value.parse() {
|
||||
style.modifier.borders.left.color = Some(c);
|
||||
}
|
||||
}
|
||||
"border-left-style" => style.modifier.borders.left.style = parse_border_style(value),
|
||||
"border-left-width" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.left.width = v;
|
||||
}
|
||||
}
|
||||
"border-radius" => {
|
||||
let values: Vec<_> = value.split(' ').collect();
|
||||
if values.len() == 1 {
|
||||
if let Some(r) = parse_value(values[0]) {
|
||||
style
|
||||
.modifier
|
||||
.borders
|
||||
.slice()
|
||||
.iter_mut()
|
||||
.for_each(|b| b.radius = r);
|
||||
}
|
||||
} else {
|
||||
for (v, b) in values
|
||||
.into_iter()
|
||||
.zip(style.modifier.borders.slice().iter_mut())
|
||||
{
|
||||
if let Some(r) = parse_value(v) {
|
||||
b.radius = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"border-right" => {}
|
||||
"border-right-color" => {
|
||||
if let Ok(c) = value.parse() {
|
||||
style.modifier.borders.right.color = Some(c);
|
||||
}
|
||||
}
|
||||
"border-right-style" => style.modifier.borders.right.style = parse_border_style(value),
|
||||
"border-right-width" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.right.width = v;
|
||||
}
|
||||
}
|
||||
"border-spacing" => {}
|
||||
"border-style" => {
|
||||
let values: Vec<_> = value.split(' ').collect();
|
||||
if values.len() == 1 {
|
||||
let border_style = parse_border_style(values[0]);
|
||||
style
|
||||
.modifier
|
||||
.borders
|
||||
.slice()
|
||||
.iter_mut()
|
||||
.for_each(|b| b.style = border_style);
|
||||
} else {
|
||||
for (v, b) in values
|
||||
.into_iter()
|
||||
.zip(style.modifier.borders.slice().iter_mut())
|
||||
{
|
||||
b.style = parse_border_style(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
"border-top" => {}
|
||||
"border-top-color" => {
|
||||
if let Ok(c) = value.parse() {
|
||||
style.modifier.borders.top.color = Some(c);
|
||||
}
|
||||
}
|
||||
"border-top-left-radius" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.left.radius = v;
|
||||
}
|
||||
}
|
||||
"border-top-right-radius" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.right.radius = v;
|
||||
}
|
||||
}
|
||||
"border-top-style" => style.modifier.borders.top.style = parse_border_style(value),
|
||||
"border-top-width" => {
|
||||
if let Some(v) = parse_value(value) {
|
||||
style.modifier.borders.top.width = v;
|
||||
}
|
||||
}
|
||||
"border-width" => {
|
||||
let values: Vec<_> = value.split(' ').collect();
|
||||
if values.len() == 1 {
|
||||
if let Some(w) = parse_value(values[0]) {
|
||||
style
|
||||
.modifier
|
||||
.borders
|
||||
.slice()
|
||||
.iter_mut()
|
||||
.for_each(|b| b.width = w);
|
||||
}
|
||||
} else {
|
||||
for (v, width) in values
|
||||
.into_iter()
|
||||
.zip(style.modifier.borders.slice().iter_mut())
|
||||
{
|
||||
if let Some(w) = parse_value(v) {
|
||||
width.width = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifier) {
|
||||
match name {
|
||||
"animation" => {}
|
||||
"animation-delay" => {}
|
||||
"animation-direction =>{}" => {}
|
||||
"animation-duration" => {}
|
||||
"animation-fill-mode" => {}
|
||||
"animation-itera =>{}tion-count" => {}
|
||||
"animation-name" => {}
|
||||
"animation-play-state" => {}
|
||||
"animation-timing-function" => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_font(name: &str, value: &str, style: &mut StyleModifier) {
|
||||
use ratatui::style::Modifier;
|
||||
match name {
|
||||
"font" => (),
|
||||
"font-family" => (),
|
||||
"font-size" => (),
|
||||
"font-size-adjust" => (),
|
||||
"font-stretch" => (),
|
||||
"font-style" => match value {
|
||||
"italic" => style.core = style.core.add_modifier(Modifier::ITALIC),
|
||||
"oblique" => style.core = style.core.add_modifier(Modifier::ITALIC),
|
||||
_ => (),
|
||||
},
|
||||
"font-variant" => todo!(),
|
||||
"font-weight" => match value {
|
||||
"bold" => style.core = style.core.add_modifier(Modifier::BOLD),
|
||||
"normal" => style.core = style.core.remove_modifier(Modifier::BOLD),
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_text(name: &str, value: &str, style: &mut StyleModifier) {
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
match name {
|
||||
"text-align" => todo!(),
|
||||
"text-align-last" => todo!(),
|
||||
"text-decoration" | "text-decoration-line" => {
|
||||
for v in value.split(' ') {
|
||||
match v {
|
||||
"line-through" => style.core = style.core.add_modifier(Modifier::CROSSED_OUT),
|
||||
"underline" => style.core = style.core.add_modifier(Modifier::UNDERLINED),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
"text-decoration-color" => todo!(),
|
||||
"text-decoration-style" => todo!(),
|
||||
"text-indent" => todo!(),
|
||||
"text-justify" => todo!(),
|
||||
"text-overflow" => todo!(),
|
||||
"text-shadow" => todo!(),
|
||||
"text-transform" => todo!(),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifier) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
const SORTED_STYLE_ATTRS: &[&str] = &[
|
||||
"animation",
|
||||
"animation-delay",
|
||||
"animation-direction",
|
||||
"animation-duration",
|
||||
"animation-fill-mode",
|
||||
"animation-iteration-count",
|
||||
"animation-name",
|
||||
"animation-play-state",
|
||||
"animation-timing-function",
|
||||
"backface-visibility",
|
||||
"background",
|
||||
"background-attachment",
|
||||
"background-clip",
|
||||
"background-color",
|
||||
"background-image",
|
||||
"background-origin",
|
||||
"background-position",
|
||||
"background-repeat",
|
||||
"background-size",
|
||||
"border",
|
||||
"border-bottom",
|
||||
"border-bottom-color",
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-bottom-style",
|
||||
"border-bottom-width",
|
||||
"border-collapse",
|
||||
"border-color",
|
||||
"border-image",
|
||||
"border-image-outset",
|
||||
"border-image-repeat",
|
||||
"border-image-slice",
|
||||
"border-image-source",
|
||||
"border-image-width",
|
||||
"border-left",
|
||||
"border-left-color",
|
||||
"border-left-style",
|
||||
"border-left-width",
|
||||
"border-radius",
|
||||
"border-right",
|
||||
"border-right-color",
|
||||
"border-right-style",
|
||||
"border-right-width",
|
||||
"border-spacing",
|
||||
"border-style",
|
||||
"border-top",
|
||||
"border-top-color",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"border-top-style",
|
||||
"border-top-width",
|
||||
"border-width",
|
||||
"bottom",
|
||||
"box-shadow",
|
||||
"box-sizing",
|
||||
"caption-side",
|
||||
"clear",
|
||||
"clip",
|
||||
"color",
|
||||
"columns",
|
||||
"content",
|
||||
"counter-increment",
|
||||
"counter-reset",
|
||||
"cursor",
|
||||
"empty-cells",
|
||||
"float",
|
||||
"font",
|
||||
"font-family",
|
||||
"font-size",
|
||||
"font-size-adjust",
|
||||
"font-stretch",
|
||||
"font-style",
|
||||
"font-variant",
|
||||
"font-weight",
|
||||
"letter-spacing",
|
||||
"line-height",
|
||||
"list-style",
|
||||
"list-style-image",
|
||||
"list-style-position",
|
||||
"list-style-type",
|
||||
"opacity",
|
||||
"order",
|
||||
"outline",
|
||||
"outline-color",
|
||||
"outline-offset",
|
||||
"outline-style",
|
||||
"outline-width",
|
||||
"page-break-after",
|
||||
"page-break-before",
|
||||
"page-break-inside",
|
||||
"perspective",
|
||||
"perspective-origin",
|
||||
"pointer-events",
|
||||
"quotes",
|
||||
"resize",
|
||||
"tab-size",
|
||||
"table-layout",
|
||||
"text-align",
|
||||
"text-align-last",
|
||||
"text-decoration",
|
||||
"text-decoration-color",
|
||||
"text-decoration-line",
|
||||
"text-decoration-style",
|
||||
"text-indent",
|
||||
"text-justify",
|
||||
"text-overflow",
|
||||
"text-shadow",
|
||||
"text-transform",
|
||||
"transition",
|
||||
"transition-delay",
|
||||
"transition-duration",
|
||||
"transition-property",
|
||||
"transition-timing-function",
|
||||
"visibility",
|
||||
"white-space",
|
||||
"background-color",
|
||||
"background",
|
||||
"background-attachment",
|
||||
"background-clip",
|
||||
"background-image",
|
||||
"background-origin",
|
||||
"background-position",
|
||||
"background-repeat",
|
||||
"background-size",
|
||||
"dotted",
|
||||
"dashed",
|
||||
"solid",
|
||||
"double",
|
||||
"groove",
|
||||
"ridge",
|
||||
"inset",
|
||||
"outset",
|
||||
"none",
|
||||
"hidden",
|
||||
"border",
|
||||
"border-bottom",
|
||||
"border-bottom-color",
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-bottom-style",
|
||||
"border-bottom-width",
|
||||
"border-collapse",
|
||||
"border-color",
|
||||
"border-image",
|
||||
"border-image-outset",
|
||||
"border-image-repeat",
|
||||
"border-image-slice",
|
||||
"border-image-source",
|
||||
"border-image-width",
|
||||
"border-left",
|
||||
"border-left-color",
|
||||
"border-left-style",
|
||||
"border-left-width",
|
||||
"border-radius",
|
||||
"border-right",
|
||||
"border-right-color",
|
||||
"border-right-style",
|
||||
"border-right-width",
|
||||
"border-spacing",
|
||||
"border-style",
|
||||
"border-top",
|
||||
"border-top-color",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"border-top-style",
|
||||
"border-top-width",
|
||||
"border-width",
|
||||
"animation",
|
||||
"animation-delay",
|
||||
"animation-direction",
|
||||
"animation-duration",
|
||||
"animation-fill-mode",
|
||||
"animation-itera ",
|
||||
"animation-name",
|
||||
"animation-play-state",
|
||||
"animation-timing-function",
|
||||
"font",
|
||||
"font-family",
|
||||
"font-size",
|
||||
"font-size-adjust",
|
||||
"font-stretch",
|
||||
"font-style",
|
||||
"italic",
|
||||
"oblique",
|
||||
"font-variant",
|
||||
"font-weight",
|
||||
"bold",
|
||||
"normal",
|
||||
"text-align",
|
||||
"text-align-last",
|
||||
"text-decoration",
|
||||
"text-decoration-line",
|
||||
"line-through",
|
||||
"underline",
|
||||
"text-decoration-color",
|
||||
"text-decoration-style",
|
||||
"text-indent",
|
||||
"text-justify",
|
||||
"text-overflow",
|
||||
"text-shadow",
|
||||
"text-transform",
|
||||
];
|
||||
@@ -0,0 +1,101 @@
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
style::{convert, RinkColor, RinkStyle},
|
||||
Config,
|
||||
};
|
||||
|
||||
pub struct RinkBuffer<'a> {
|
||||
buf: &'a mut Buffer,
|
||||
cfg: Config,
|
||||
}
|
||||
|
||||
impl<'a> RinkBuffer<'a> {
|
||||
fn new(buf: &'a mut Buffer, cfg: Config) -> RinkBuffer<'a> {
|
||||
Self { buf, cfg }
|
||||
}
|
||||
|
||||
pub fn set(&mut self, x: u16, y: u16, new: RinkCell) {
|
||||
let area = self.buf.area();
|
||||
if x < area.x || x >= area.width + area.x || y < area.y || y >= area.height + area.y {
|
||||
// panic!("({x}, {y}) is not in {area:?}");
|
||||
return;
|
||||
}
|
||||
let cell = self.buf.get_mut(x, y);
|
||||
cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
|
||||
if new.symbol.is_empty() {
|
||||
if !cell.symbol.is_empty() {
|
||||
// allows text to "shine through" transparent backgrounds
|
||||
cell.fg = convert(self.cfg.rendering_mode, new.bg.blend(cell.fg));
|
||||
}
|
||||
} else {
|
||||
cell.modifier = new.modifier;
|
||||
cell.symbol = new.symbol;
|
||||
cell.fg = convert(self.cfg.rendering_mode, new.fg.blend(cell.bg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RinkWidget {
|
||||
fn render(self, area: Rect, buf: RinkBuffer);
|
||||
}
|
||||
|
||||
pub struct WidgetWithContext<T: RinkWidget> {
|
||||
widget: T,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl<T: RinkWidget> WidgetWithContext<T> {
|
||||
pub fn new(widget: T, config: Config) -> WidgetWithContext<T> {
|
||||
WidgetWithContext { widget, config }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RinkWidget> Widget for WidgetWithContext<T> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.widget.render(area, RinkBuffer::new(buf, self.config));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RinkCell {
|
||||
pub symbol: String,
|
||||
pub bg: RinkColor,
|
||||
pub fg: RinkColor,
|
||||
pub modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for RinkCell {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
symbol: "".to_string(),
|
||||
fg: RinkColor {
|
||||
color: Color::Rgb(0, 0, 0),
|
||||
alpha: 0,
|
||||
},
|
||||
bg: RinkColor {
|
||||
color: Color::Rgb(0, 0, 0),
|
||||
alpha: 0,
|
||||
},
|
||||
modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkCell {
|
||||
pub fn set_style(&mut self, style: RinkStyle) {
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
self.modifier = style.add_modifier;
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData};
|
||||
use dioxus_native_core::{
|
||||
custom_element::CustomElement,
|
||||
node::OwnedAttributeDiscription,
|
||||
node_ref::AttributeMask,
|
||||
prelude::NodeType,
|
||||
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
|
||||
NodeId,
|
||||
};
|
||||
use shipyard::UniqueView;
|
||||
|
||||
use crate::hooks::FormData;
|
||||
|
||||
use super::{RinkWidget, WidgetContext};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Button {
|
||||
text_id: NodeId,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
fn width(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn height(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "height".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
|
||||
let width = Self::width(el);
|
||||
let height = Self::height(el);
|
||||
let single_char = width == "1px" || height == "1px";
|
||||
let border_style = if single_char { "none" } else { "solid" };
|
||||
el.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "border-style".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
border_style.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
fn update_value_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
self.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
fn write_value(&self, rdom: &mut RealDom) {
|
||||
if let Some(mut text) = rdom.get_mut(self.text_id) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.value.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn switch(&mut self, ctx: &WidgetContext, node: NodeMut) {
|
||||
let data = FormData {
|
||||
value: self.value.to_string(),
|
||||
values: HashMap::new(),
|
||||
files: None,
|
||||
};
|
||||
ctx.send(crate::Event {
|
||||
id: node.id(),
|
||||
name: "input",
|
||||
data: crate::EventData::Form(data),
|
||||
bubbles: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomElement for Button {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
vec![self.text_id]
|
||||
}
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el
|
||||
.attributes
|
||||
.get(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string());
|
||||
|
||||
drop(node_type);
|
||||
|
||||
let rdom = root.real_dom_mut();
|
||||
let text = rdom.create_node(value.clone().unwrap_or_default());
|
||||
let text_id = text.id();
|
||||
|
||||
root.add_event_listener("keydown");
|
||||
root.add_event_listener("click");
|
||||
|
||||
Self {
|
||||
text_id,
|
||||
value: value.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
mut root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
match attributes {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
self.write_value(root.real_dom_mut());
|
||||
}
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
self.update_value_attr(&el);
|
||||
}
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
self.write_value(root.real_dom_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for Button {
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: &crate::Event,
|
||||
mut node: dioxus_native_core::real_dom::NodeMut,
|
||||
) {
|
||||
let ctx: WidgetContext = {
|
||||
node.real_dom_mut()
|
||||
.raw_world_mut()
|
||||
.borrow::<UniqueView<WidgetContext>>()
|
||||
.expect("expected widget context")
|
||||
.clone()
|
||||
};
|
||||
|
||||
match event.name {
|
||||
"click" => self.switch(&ctx, node),
|
||||
"keydown" => {
|
||||
if let crate::EventData::Keyboard(data) = &event.data {
|
||||
if !data.is_auto_repeating()
|
||||
&& match data.key() {
|
||||
Key::Character(c) if c == " " => true,
|
||||
Key::Enter => true,
|
||||
_ => false,
|
||||
}
|
||||
{
|
||||
self.switch(&ctx, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData};
|
||||
use dioxus_native_core::{
|
||||
custom_element::CustomElement,
|
||||
node::OwnedAttributeDiscription,
|
||||
node_ref::AttributeMask,
|
||||
prelude::NodeType,
|
||||
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut},
|
||||
NodeId,
|
||||
};
|
||||
use shipyard::UniqueView;
|
||||
|
||||
use crate::hooks::FormData;
|
||||
|
||||
use super::{RinkWidget, WidgetContext};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct CheckBox {
|
||||
div_id: NodeId,
|
||||
text_id: NodeId,
|
||||
value: String,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
impl CheckBox {
|
||||
fn width(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn height(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "height".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
|
||||
let width = Self::width(el);
|
||||
let height = Self::height(el);
|
||||
let single_char = width == "1px" || height == "1px";
|
||||
let border_style = if single_char { "none" } else { "solid" };
|
||||
el.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "border-style".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
border_style.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
fn update_value_attr(&mut self, el: &ElementNodeMut) {
|
||||
self.value = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "on".to_string());
|
||||
}
|
||||
|
||||
fn update_checked_attr(&mut self, el: &ElementNodeMut) {
|
||||
self.checked = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "checked".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "false".to_string())
|
||||
== "true";
|
||||
}
|
||||
|
||||
fn write_value(&self, mut root: NodeMut) {
|
||||
let single_char = {
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
Self::width(&el) == "1px" || Self::height(&el) == "1px"
|
||||
};
|
||||
let rdom = root.real_dom_mut();
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.text_id) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
let value = if single_char {
|
||||
if self.checked {
|
||||
"☑"
|
||||
} else {
|
||||
"☐"
|
||||
}
|
||||
} else if self.checked {
|
||||
"✓"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
*text.text_mut() = value.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn switch(&mut self, mut node: NodeMut) {
|
||||
let new_state = !self.checked;
|
||||
|
||||
let data = FormData {
|
||||
value: new_state
|
||||
.then(|| self.value.to_string())
|
||||
.unwrap_or_default(),
|
||||
values: HashMap::new(),
|
||||
files: None,
|
||||
};
|
||||
{
|
||||
let ctx: UniqueView<WidgetContext> = node
|
||||
.real_dom_mut()
|
||||
.raw_world_mut()
|
||||
.borrow()
|
||||
.expect("expected widget context");
|
||||
ctx.send(crate::Event {
|
||||
id: self.div_id,
|
||||
name: "input",
|
||||
data: crate::EventData::Form(data),
|
||||
bubbles: true,
|
||||
});
|
||||
}
|
||||
|
||||
self.checked = new_state;
|
||||
|
||||
self.write_value(node);
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomElement for CheckBox {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
vec![self.text_id]
|
||||
}
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el
|
||||
.attributes
|
||||
.get(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string());
|
||||
|
||||
drop(node_type);
|
||||
|
||||
let rdom = root.real_dom_mut();
|
||||
let text = rdom.create_node(String::new());
|
||||
let text_id = text.id();
|
||||
|
||||
root.add_event_listener("click");
|
||||
root.add_event_listener("keydown");
|
||||
let div_id = root.id();
|
||||
|
||||
let myself = Self {
|
||||
div_id,
|
||||
text_id,
|
||||
value: value.unwrap_or_default(),
|
||||
checked: false,
|
||||
};
|
||||
myself.write_value(root);
|
||||
|
||||
myself
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
mut root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
match attributes {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
self.update_checked_attr(&el);
|
||||
}
|
||||
self.write_value(root);
|
||||
}
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
self.update_value_attr(&el);
|
||||
}
|
||||
if attrs.contains("checked") {
|
||||
self.update_checked_attr(&el);
|
||||
}
|
||||
}
|
||||
if attrs.contains("checked") {
|
||||
self.write_value(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for CheckBox {
|
||||
fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
|
||||
match event.name {
|
||||
"click" => self.switch(node),
|
||||
"keydown" => {
|
||||
if let crate::EventData::Keyboard(data) = &event.data {
|
||||
if !data.is_auto_repeating()
|
||||
&& match data.key() {
|
||||
Key::Character(c) if c == " " => true,
|
||||
Key::Enter => true,
|
||||
_ => false,
|
||||
}
|
||||
{
|
||||
self.switch(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use dioxus_native_core::{
|
||||
custom_element::CustomElement, node::OwnedAttributeDiscription, prelude::NodeType,
|
||||
real_dom::NodeImmutable,
|
||||
};
|
||||
|
||||
use super::{
|
||||
checkbox::CheckBox, number::Number, password::Password, slider::Slider, textbox::TextBox,
|
||||
RinkWidget,
|
||||
};
|
||||
use crate::widgets::button::Button;
|
||||
|
||||
pub(crate) enum Input {
|
||||
Button(Button),
|
||||
CheckBox(CheckBox),
|
||||
TextBox(TextBox),
|
||||
Password(Password),
|
||||
Number(Number),
|
||||
Slider(Slider),
|
||||
}
|
||||
|
||||
impl CustomElement for Input {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
|
||||
match self {
|
||||
Input::Button(button) => button.roots(),
|
||||
Input::CheckBox(checkbox) => checkbox.roots(),
|
||||
Input::TextBox(textbox) => textbox.roots(),
|
||||
Input::Password(password) => password.roots(),
|
||||
Input::Number(number) => number.roots(),
|
||||
Input::Slider(slider) => slider.roots(),
|
||||
}
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
|
||||
match self {
|
||||
Input::Button(button) => button.slot(),
|
||||
Input::CheckBox(checkbox) => checkbox.slot(),
|
||||
Input::TextBox(textbox) => textbox.slot(),
|
||||
Input::Password(password) => password.slot(),
|
||||
Input::Number(number) => number.slot(),
|
||||
Input::Slider(slider) => slider.slot(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
{
|
||||
// currently widgets are not allowed to have children
|
||||
let children = root.child_ids();
|
||||
let rdom = root.real_dom_mut();
|
||||
for child in children {
|
||||
if let Some(mut child) = rdom.get_mut(child) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
let input_type = el
|
||||
.attributes
|
||||
.get(&OwnedAttributeDiscription {
|
||||
name: "type".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text());
|
||||
match input_type
|
||||
.map(|type_| type_.trim().to_lowercase())
|
||||
.as_deref()
|
||||
{
|
||||
Some("button") => {
|
||||
drop(node_type);
|
||||
Input::Button(Button::create(root))
|
||||
}
|
||||
Some("checkbox") => {
|
||||
drop(node_type);
|
||||
Input::CheckBox(CheckBox::create(root))
|
||||
}
|
||||
Some("textbox") => {
|
||||
drop(node_type);
|
||||
Input::TextBox(TextBox::create(root))
|
||||
}
|
||||
Some("password") => {
|
||||
drop(node_type);
|
||||
Input::Password(Password::create(root))
|
||||
}
|
||||
Some("number") => {
|
||||
drop(node_type);
|
||||
Input::Number(Number::create(root))
|
||||
}
|
||||
Some("range") => {
|
||||
drop(node_type);
|
||||
Input::Slider(Slider::create(root))
|
||||
}
|
||||
_ => {
|
||||
drop(node_type);
|
||||
Input::TextBox(TextBox::create(root))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
match self {
|
||||
Input::Button(button) => {
|
||||
button.attributes_changed(root, attributes);
|
||||
}
|
||||
Input::CheckBox(checkbox) => {
|
||||
checkbox.attributes_changed(root, attributes);
|
||||
}
|
||||
Input::TextBox(textbox) => {
|
||||
textbox.attributes_changed(root, attributes);
|
||||
}
|
||||
Input::Password(password) => {
|
||||
password.attributes_changed(root, attributes);
|
||||
}
|
||||
Input::Number(number) => {
|
||||
number.attributes_changed(root, attributes);
|
||||
}
|
||||
Input::Slider(slider) => {
|
||||
slider.attributes_changed(root, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for Input {
|
||||
fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
|
||||
match self {
|
||||
Input::Button(button) => {
|
||||
button.handle_event(event, node);
|
||||
}
|
||||
Input::CheckBox(checkbox) => {
|
||||
checkbox.handle_event(event, node);
|
||||
}
|
||||
Input::TextBox(textbox) => {
|
||||
textbox.handle_event(event, node);
|
||||
}
|
||||
Input::Password(password) => {
|
||||
password.handle_event(event, node);
|
||||
}
|
||||
Input::Number(number) => {
|
||||
number.handle_event(event, node);
|
||||
}
|
||||
Input::Slider(slider) => {
|
||||
slider.handle_event(event, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
mod button;
|
||||
mod checkbox;
|
||||
mod input;
|
||||
mod number;
|
||||
mod password;
|
||||
mod slider;
|
||||
mod text_like;
|
||||
mod textbox;
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use dioxus_native_core::{
|
||||
custom_element::{CustomElement, CustomElementUpdater},
|
||||
real_dom::{NodeMut, RealDom},
|
||||
};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use shipyard::{Component, Unique};
|
||||
|
||||
use crate::Event;
|
||||
|
||||
pub(crate) fn register_widgets(rdom: &mut RealDom, sender: UnboundedSender<Event>) {
|
||||
// inject the widget context
|
||||
rdom.raw_world().add_unique(WidgetContext { sender });
|
||||
|
||||
rdom.register_custom_element::<RinkWidgetWrapper<input::Input>>();
|
||||
}
|
||||
|
||||
trait RinkWidget: Sync + Send + CustomElement + 'static {
|
||||
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut);
|
||||
}
|
||||
|
||||
pub trait RinkWidgetResponder: CustomElementUpdater {
|
||||
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut);
|
||||
}
|
||||
|
||||
impl<W: RinkWidget> RinkWidgetResponder for W {
|
||||
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) {
|
||||
RinkWidget::handle_event(self, event, node)
|
||||
}
|
||||
}
|
||||
|
||||
struct RinkWidgetWrapper<W: RinkWidget> {
|
||||
inner: RinkWidgetTraitObject,
|
||||
_marker: std::marker::PhantomData<W>,
|
||||
}
|
||||
|
||||
impl<W: RinkWidget> CustomElement for RinkWidgetWrapper<W> {
|
||||
const NAME: &'static str = W::NAME;
|
||||
|
||||
const NAMESPACE: Option<&'static str> = W::NAMESPACE;
|
||||
|
||||
fn create(mut node: NodeMut) -> Self {
|
||||
let myself = RinkWidgetTraitObject {
|
||||
widget: Arc::new(RwLock::new(W::create(node.reborrow()))),
|
||||
};
|
||||
|
||||
// Insert the widget as an arbitrary data node so that it can be recognized when bubbling events
|
||||
node.insert(myself.clone());
|
||||
|
||||
RinkWidgetWrapper {
|
||||
inner: myself,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
let mut widget = self.inner.widget.write().unwrap();
|
||||
widget.attributes_changed(root, attributes);
|
||||
}
|
||||
|
||||
fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
|
||||
let widget = self.inner.widget.read().unwrap();
|
||||
widget.roots()
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
|
||||
let widget = self.inner.widget.read().unwrap();
|
||||
widget.slot()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Component)]
|
||||
pub(crate) struct RinkWidgetTraitObject {
|
||||
widget: Arc<RwLock<dyn RinkWidgetResponder + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl CustomElementUpdater for RinkWidgetTraitObject {
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
light_root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
let mut widget = self.widget.write().unwrap();
|
||||
widget.attributes_changed(light_root, attributes);
|
||||
}
|
||||
|
||||
fn roots(&self) -> Vec<dioxus_native_core::NodeId> {
|
||||
let widget = self.widget.read().unwrap();
|
||||
widget.roots()
|
||||
}
|
||||
|
||||
fn slot(&self) -> Option<dioxus_native_core::NodeId> {
|
||||
let widget = self.widget.read().unwrap();
|
||||
widget.slot()
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidgetResponder for RinkWidgetTraitObject {
|
||||
fn handle_event(&mut self, event: &Event, node: dioxus_native_core::real_dom::NodeMut) {
|
||||
let mut widget = self.widget.write().unwrap();
|
||||
widget.handle_event(event, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Unique, Clone)]
|
||||
pub(crate) struct WidgetContext {
|
||||
sender: UnboundedSender<Event>,
|
||||
}
|
||||
|
||||
impl WidgetContext {
|
||||
pub(crate) fn send(&self, event: Event) {
|
||||
self.sender.unbounded_send(event).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use dioxus_html::{input_data::keyboard_types::Key, HasKeyboardData};
|
||||
use dioxus_native_core::{
|
||||
custom_element::CustomElement,
|
||||
real_dom::{NodeImmutable, RealDom},
|
||||
NodeId,
|
||||
};
|
||||
|
||||
use crate::EventData;
|
||||
|
||||
use super::{text_like::TextLike, RinkWidget};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Number {
|
||||
text: TextLike,
|
||||
}
|
||||
|
||||
impl Number {
|
||||
fn increase(&mut self, rdom: &mut RealDom, id: NodeId) {
|
||||
let num = self.text.text().parse::<f64>().unwrap_or(0.0);
|
||||
self.text.set_text((num + 1.0).to_string(), rdom, id);
|
||||
}
|
||||
|
||||
fn decrease(&mut self, rdom: &mut RealDom, id: NodeId) {
|
||||
let num = self.text.text().parse::<f64>().unwrap_or(0.0);
|
||||
self.text.set_text((num - 1.0).to_string(), rdom, id);
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomElement for Number {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
self.text.roots()
|
||||
}
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
Number {
|
||||
text: TextLike::create(root.reborrow()),
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
self.text.attributes_changed(root, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for Number {
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
event: &crate::Event,
|
||||
mut node: dioxus_native_core::real_dom::NodeMut,
|
||||
) {
|
||||
if event.name == "keydown" {
|
||||
if let EventData::Keyboard(data) = &event.data {
|
||||
let key = data.key();
|
||||
let is_num_like = match key.clone() {
|
||||
Key::ArrowLeft | Key::ArrowRight | Key::Backspace => true,
|
||||
Key::Character(c)
|
||||
if c == "." || c == "-" || c.chars().all(|c| c.is_numeric()) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_num_like {
|
||||
self.text.handle_event(event, node)
|
||||
} else {
|
||||
let id = node.id();
|
||||
let rdom = node.real_dom_mut();
|
||||
match key {
|
||||
Key::ArrowUp => {
|
||||
self.increase(rdom, id);
|
||||
}
|
||||
Key::ArrowDown => {
|
||||
self.decrease(rdom, id);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.text.handle_event(event, node)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use super::text_like::{TextLike, TextLikeController};
|
||||
|
||||
pub(crate) type Password = TextLike<PasswordController>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PasswordController;
|
||||
|
||||
impl TextLikeController for PasswordController {
|
||||
fn display_text(&self, text: &str) -> String {
|
||||
text.chars().map(|_| '.').collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus_html::{
|
||||
input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData,
|
||||
SerializedMouseData,
|
||||
};
|
||||
use dioxus_native_core::{
|
||||
custom_element::CustomElement,
|
||||
node::{OwnedAttributeDiscription, OwnedAttributeValue},
|
||||
node_ref::AttributeMask,
|
||||
prelude::{ElementNode, NodeType},
|
||||
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
|
||||
NodeId,
|
||||
};
|
||||
use shipyard::UniqueView;
|
||||
|
||||
use super::{RinkWidget, WidgetContext};
|
||||
use crate::hooks::FormData;
|
||||
use crate::{query::get_layout, Event, EventData, Query};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Slider {
|
||||
div_wrapper: NodeId,
|
||||
pre_cursor_div: NodeId,
|
||||
post_cursor_div: NodeId,
|
||||
min: f64,
|
||||
max: f64,
|
||||
step: Option<f64>,
|
||||
value: f64,
|
||||
border: bool,
|
||||
}
|
||||
|
||||
impl Default for Slider {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
div_wrapper: Default::default(),
|
||||
pre_cursor_div: Default::default(),
|
||||
post_cursor_div: Default::default(),
|
||||
min: 0.0,
|
||||
max: 100.0,
|
||||
step: None,
|
||||
value: 0.0,
|
||||
border: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Slider {
|
||||
fn size(&self) -> f64 {
|
||||
self.max - self.min
|
||||
}
|
||||
|
||||
fn step(&self) -> f64 {
|
||||
self.step.unwrap_or(self.size() / 10.0)
|
||||
}
|
||||
|
||||
fn width(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn height(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "height".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_min_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "min".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
self.min = value.parse().ok().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_max_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "max".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
self.max = value.parse().ok().unwrap_or(100.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_step_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "step".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
self.step = value.parse().ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
|
||||
let width = Self::width(el);
|
||||
let height = Self::height(el);
|
||||
let single_char = width
|
||||
.strip_prefix("px")
|
||||
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_prefix("px")
|
||||
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
|
||||
.is_some();
|
||||
self.border = !single_char;
|
||||
let border_style = if self.border { "solid" } else { "none" };
|
||||
el.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "border-style".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
border_style.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
fn update_value(&mut self, new: f64) {
|
||||
self.value = new.clamp(self.min, self.max);
|
||||
}
|
||||
|
||||
fn update_value_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
self.update_value(value.parse().ok().unwrap_or(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
|
||||
let value_percent = (self.value - self.min) / self.size() * 100.0;
|
||||
|
||||
if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) {
|
||||
let node_type = div.node_type_mut();
|
||||
let NodeTypeMut::Element(mut element) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
element.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
format!("{}%", value_percent),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(mut div) = rdom.get_mut(self.post_cursor_div) {
|
||||
let node_type = div.node_type_mut();
|
||||
let NodeTypeMut::Element(mut element) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
element.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
format!("{}%", 100.0 - value_percent),
|
||||
);
|
||||
}
|
||||
|
||||
// send the event
|
||||
let world = rdom.raw_world_mut();
|
||||
|
||||
{
|
||||
let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
|
||||
|
||||
let data = FormData {
|
||||
value: self.value.to_string(),
|
||||
values: HashMap::new(),
|
||||
files: None,
|
||||
};
|
||||
ctx.send(Event {
|
||||
id,
|
||||
name: "input",
|
||||
data: EventData::Form(data),
|
||||
bubbles: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) {
|
||||
let key = data.key();
|
||||
|
||||
let step = self.step();
|
||||
match key {
|
||||
Key::ArrowDown | Key::ArrowLeft => {
|
||||
self.update_value(self.value - step);
|
||||
}
|
||||
Key::ArrowUp | Key::ArrowRight => {
|
||||
self.update_value(self.value + step);
|
||||
}
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let id = root.id();
|
||||
|
||||
let rdom = root.real_dom_mut();
|
||||
self.write_value(rdom, id);
|
||||
}
|
||||
|
||||
fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) {
|
||||
if !data.held_buttons().is_empty() {
|
||||
let id = root.id();
|
||||
let rdom = root.real_dom_mut();
|
||||
let world = rdom.raw_world_mut();
|
||||
let taffy = {
|
||||
let query: UniqueView<Query> = world.borrow().unwrap();
|
||||
query.stretch.clone()
|
||||
};
|
||||
|
||||
let taffy = taffy.lock().unwrap();
|
||||
|
||||
let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
|
||||
|
||||
let width = layout.size.width as f64;
|
||||
let offset = data.element_coordinates();
|
||||
self.update_value(self.min + self.size() * offset.x / width);
|
||||
|
||||
self.write_value(rdom, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomElement for Slider {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
vec![self.div_wrapper]
|
||||
}
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el.attributes.get(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
});
|
||||
let value = value
|
||||
.and_then(|value| match value {
|
||||
OwnedAttributeValue::Text(text) => text.as_str().parse().ok(),
|
||||
OwnedAttributeValue::Float(float) => Some(*float),
|
||||
OwnedAttributeValue::Int(int) => Some(*int as f64),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
|
||||
drop(node_type);
|
||||
|
||||
let rdom = root.real_dom_mut();
|
||||
|
||||
let pre_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
|
||||
tag: "div".to_string(),
|
||||
attributes: [(
|
||||
OwnedAttributeDiscription {
|
||||
name: "background-color".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"rgba(10,10,10,0.5)".to_string().into(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
let pre_cursor_div_id = pre_cursor_div.id();
|
||||
|
||||
let cursor_text = rdom.create_node("|".to_string());
|
||||
let cursor_text_id = cursor_text.id();
|
||||
let mut cursor_span = rdom.create_node(NodeType::Element(ElementNode {
|
||||
tag: "div".to_string(),
|
||||
attributes: [].into_iter().collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
cursor_span.add_child(cursor_text_id);
|
||||
let cursor_span_id = cursor_span.id();
|
||||
|
||||
let post_cursor_div = rdom.create_node(NodeType::Element(ElementNode {
|
||||
tag: "span".to_string(),
|
||||
attributes: [
|
||||
(
|
||||
OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"100%".to_string().into(),
|
||||
),
|
||||
(
|
||||
OwnedAttributeDiscription {
|
||||
name: "background-color".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"rgba(10,10,10,0.5)".to_string().into(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
let post_cursor_div_id = post_cursor_div.id();
|
||||
|
||||
let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
|
||||
tag: "div".to_string(),
|
||||
attributes: [
|
||||
(
|
||||
OwnedAttributeDiscription {
|
||||
name: "display".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"flex".to_string().into(),
|
||||
),
|
||||
(
|
||||
OwnedAttributeDiscription {
|
||||
name: "flex-direction".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"row".to_string().into(),
|
||||
),
|
||||
(
|
||||
OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"100%".to_string().into(),
|
||||
),
|
||||
(
|
||||
OwnedAttributeDiscription {
|
||||
name: "height".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"100%".to_string().into(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
let div_wrapper_id = div_wrapper.id();
|
||||
div_wrapper.add_child(pre_cursor_div_id);
|
||||
div_wrapper.add_child(cursor_span_id);
|
||||
div_wrapper.add_child(post_cursor_div_id);
|
||||
|
||||
root.add_event_listener("mousemove");
|
||||
root.add_event_listener("mousedown");
|
||||
root.add_event_listener("keydown");
|
||||
|
||||
Self {
|
||||
pre_cursor_div: pre_cursor_div_id,
|
||||
post_cursor_div: post_cursor_div_id,
|
||||
div_wrapper: div_wrapper_id,
|
||||
value,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
mut root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
match attributes {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
self.update_max_attr(&el);
|
||||
self.update_min_attr(&el);
|
||||
self.update_step_attr(&el);
|
||||
}
|
||||
let id = root.id();
|
||||
self.write_value(root.real_dom_mut(), id);
|
||||
}
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
if attrs.contains("max") {
|
||||
self.update_max_attr(&el);
|
||||
}
|
||||
if attrs.contains("min") {
|
||||
self.update_min_attr(&el);
|
||||
}
|
||||
if attrs.contains("step") {
|
||||
self.update_step_attr(&el);
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
self.update_value_attr(&el);
|
||||
}
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
let id = root.id();
|
||||
self.write_value(root.real_dom_mut(), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for Slider {
|
||||
fn handle_event(&mut self, event: &crate::Event, node: dioxus_native_core::real_dom::NodeMut) {
|
||||
match event.name {
|
||||
"keydown" => {
|
||||
if let EventData::Keyboard(data) = &event.data {
|
||||
self.handle_keydown(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
"mousemove" => {
|
||||
if let EventData::Mouse(data) = &event.data {
|
||||
self.handle_mousemove(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
"mousedown" => {
|
||||
if let EventData::Mouse(data) = &event.data {
|
||||
self.handle_mousemove(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
use std::{collections::HashMap, io::stdout};
|
||||
|
||||
use crossterm::{cursor::MoveTo, execute};
|
||||
use dioxus_html::{
|
||||
input_data::keyboard_types::Key, prelude::*, HasKeyboardData, SerializedKeyboardData,
|
||||
SerializedMouseData,
|
||||
};
|
||||
use dioxus_native_core::{
|
||||
custom_element::CustomElement,
|
||||
node::OwnedAttributeDiscription,
|
||||
node_ref::AttributeMask,
|
||||
prelude::{ElementNode, NodeType},
|
||||
real_dom::{ElementNodeMut, NodeImmutable, NodeMut, NodeTypeMut, RealDom},
|
||||
utils::cursor::{Cursor, Pos},
|
||||
NodeId,
|
||||
};
|
||||
use shipyard::UniqueView;
|
||||
use taffy::geometry::Point;
|
||||
|
||||
use crate::hooks::FormData;
|
||||
use crate::{query::get_layout, Event, EventData, Query};
|
||||
|
||||
use super::{RinkWidget, WidgetContext};
|
||||
|
||||
pub(crate) trait TextLikeController {
|
||||
fn display_text(&self, text: &str) -> String {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct EmptyController;
|
||||
|
||||
impl TextLikeController for EmptyController {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct TextLike<C: TextLikeController = EmptyController> {
|
||||
text: String,
|
||||
div_wrapper: NodeId,
|
||||
pre_cursor_text: NodeId,
|
||||
highlighted_text: NodeId,
|
||||
post_cursor_text: NodeId,
|
||||
cursor: Cursor,
|
||||
dragging: bool,
|
||||
border: bool,
|
||||
max_len: Option<usize>,
|
||||
controller: C,
|
||||
}
|
||||
|
||||
impl<C: TextLikeController> TextLike<C> {
|
||||
fn width(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "width".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn height(el: &ElementNodeMut) -> String {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "height".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
value
|
||||
} else {
|
||||
"1px".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_max_width_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "maxlength".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
if let Ok(max_len) = value.parse::<usize>() {
|
||||
self.max_len = Some(max_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_size_attr(&mut self, el: &mut ElementNodeMut) {
|
||||
let width = Self::width(el);
|
||||
let height = Self::height(el);
|
||||
let single_char = width
|
||||
.strip_prefix("px")
|
||||
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
|
||||
.is_some()
|
||||
|| height
|
||||
.strip_prefix("px")
|
||||
.and_then(|n| n.parse::<u32>().ok().filter(|num| *num > 3))
|
||||
.is_some();
|
||||
self.border = !single_char;
|
||||
let border_style = if self.border { "solid" } else { "none" };
|
||||
el.set_attribute(
|
||||
OwnedAttributeDiscription {
|
||||
name: "border-style".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
border_style.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
fn update_value_attr(&mut self, el: &ElementNodeMut) {
|
||||
if let Some(value) = el
|
||||
.get_attribute(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string())
|
||||
{
|
||||
self.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_text(&mut self, text: String, rdom: &mut RealDom, id: NodeId) {
|
||||
self.text = text;
|
||||
self.write_value(rdom, id);
|
||||
}
|
||||
|
||||
pub(crate) fn text(&self) -> &str {
|
||||
self.text.as_str()
|
||||
}
|
||||
|
||||
fn write_value(&self, rdom: &mut RealDom, id: NodeId) {
|
||||
let start_highlight = self.cursor.first().idx(self.text.as_str());
|
||||
let end_highlight = self.cursor.last().idx(self.text.as_str());
|
||||
let (text_before_first_cursor, text_after_first_cursor) =
|
||||
self.text.split_at(start_highlight);
|
||||
let (text_highlighted, text_after_second_cursor) =
|
||||
text_after_first_cursor.split_at(end_highlight - start_highlight);
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.controller.display_text(text_before_first_cursor);
|
||||
}
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.highlighted_text) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.controller.display_text(text_highlighted);
|
||||
}
|
||||
|
||||
if let Some(mut text) = rdom.get_mut(self.post_cursor_text) {
|
||||
let node_type = text.node_type_mut();
|
||||
let NodeTypeMut::Text(mut text) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
*text.text_mut() = self.controller.display_text(text_after_second_cursor);
|
||||
}
|
||||
|
||||
// send the event
|
||||
{
|
||||
let world = rdom.raw_world_mut();
|
||||
let data: FormData = FormData {
|
||||
value: self.text.clone(),
|
||||
values: HashMap::new(),
|
||||
files: None,
|
||||
};
|
||||
let ctx: UniqueView<WidgetContext> = world.borrow().expect("expected widget context");
|
||||
|
||||
ctx.send(Event {
|
||||
id,
|
||||
name: "input",
|
||||
data: EventData::Form(data),
|
||||
bubbles: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_keydown(&mut self, mut root: NodeMut, data: &SerializedKeyboardData) {
|
||||
let key = data.key();
|
||||
let modifiers = data.modifiers();
|
||||
let code = data.code();
|
||||
|
||||
if key == Key::Enter {
|
||||
return;
|
||||
}
|
||||
self.cursor.handle_input(
|
||||
&code,
|
||||
&key,
|
||||
&modifiers,
|
||||
&mut self.text,
|
||||
self.max_len.unwrap_or(1000),
|
||||
);
|
||||
|
||||
let id = root.id();
|
||||
|
||||
let rdom = root.real_dom_mut();
|
||||
self.write_value(rdom, id);
|
||||
let world = rdom.raw_world_mut();
|
||||
|
||||
// move cursor to new position
|
||||
let taffy = {
|
||||
let query: UniqueView<Query> = world.borrow().unwrap();
|
||||
query.stretch.clone()
|
||||
};
|
||||
|
||||
let taffy = taffy.lock().unwrap();
|
||||
|
||||
let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
|
||||
let Point { x, y } = layout.location;
|
||||
|
||||
let Pos { col, row } = self.cursor.start;
|
||||
let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
|
||||
if let Ok(pos) = crossterm::cursor::position() {
|
||||
if pos != (x, y) {
|
||||
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||
}
|
||||
} else {
|
||||
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mousemove(&mut self, mut root: NodeMut, data: &SerializedMouseData) {
|
||||
if self.dragging {
|
||||
let id = root.id();
|
||||
let offset = data.element_coordinates();
|
||||
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||
|
||||
// textboxs are only one line tall
|
||||
new.row = 0;
|
||||
|
||||
if new != self.cursor.start {
|
||||
self.cursor.end = Some(new);
|
||||
}
|
||||
let rdom = root.real_dom_mut();
|
||||
self.write_value(rdom, id);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mousedown(&mut self, mut root: NodeMut, data: &SerializedMouseData) {
|
||||
let offset = data.element_coordinates();
|
||||
let mut new = Pos::new(offset.x as usize, offset.y as usize);
|
||||
|
||||
// textboxs are only one line tall
|
||||
new.row = 0;
|
||||
|
||||
new.realize_col(self.text.as_str());
|
||||
self.cursor = Cursor::from_start(new);
|
||||
self.dragging = true;
|
||||
|
||||
let id = root.id();
|
||||
|
||||
// move cursor to new position
|
||||
let rdom = root.real_dom_mut();
|
||||
let world = rdom.raw_world_mut();
|
||||
let taffy = {
|
||||
let query: UniqueView<Query> = world.borrow().unwrap();
|
||||
query.stretch.clone()
|
||||
};
|
||||
|
||||
let taffy = taffy.lock().unwrap();
|
||||
|
||||
let layout = get_layout(rdom.get(self.div_wrapper).unwrap(), &taffy).unwrap();
|
||||
let Point { x, y } = layout.location;
|
||||
|
||||
let Pos { col, row } = self.cursor.start;
|
||||
let (x, y) = (col as u16 + x as u16, row as u16 + y as u16);
|
||||
if let Ok(pos) = crossterm::cursor::position() {
|
||||
if pos != (x, y) {
|
||||
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||
}
|
||||
} else {
|
||||
execute!(stdout(), MoveTo(x, y)).unwrap();
|
||||
}
|
||||
|
||||
self.write_value(rdom, id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TextLikeController + Send + Sync + Default + 'static> CustomElement for TextLike<C> {
|
||||
const NAME: &'static str = "input";
|
||||
|
||||
fn roots(&self) -> Vec<NodeId> {
|
||||
vec![self.div_wrapper]
|
||||
}
|
||||
|
||||
fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self {
|
||||
let node_type = root.node_type();
|
||||
let NodeType::Element(el) = &*node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
|
||||
let value = el
|
||||
.attributes
|
||||
.get(&OwnedAttributeDiscription {
|
||||
name: "value".to_string(),
|
||||
namespace: None,
|
||||
})
|
||||
.and_then(|value| value.as_text())
|
||||
.map(|value| value.to_string());
|
||||
|
||||
drop(node_type);
|
||||
|
||||
let rdom = root.real_dom_mut();
|
||||
|
||||
let pre_text = rdom.create_node(String::new());
|
||||
let pre_text_id = pre_text.id();
|
||||
let highlighted_text = rdom.create_node(String::new());
|
||||
let highlighted_text_id = highlighted_text.id();
|
||||
let mut highlighted_text_span = rdom.create_node(NodeType::Element(ElementNode {
|
||||
tag: "span".to_string(),
|
||||
attributes: [(
|
||||
OwnedAttributeDiscription {
|
||||
name: "background-color".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"rgba(255, 255, 255, 50%)".to_string().into(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
highlighted_text_span.add_child(highlighted_text_id);
|
||||
let highlighted_text_span_id = highlighted_text_span.id();
|
||||
let post_text = rdom.create_node(value.clone().unwrap_or_default());
|
||||
let post_text_id = post_text.id();
|
||||
let mut div_wrapper = rdom.create_node(NodeType::Element(ElementNode {
|
||||
tag: "div".to_string(),
|
||||
attributes: [(
|
||||
OwnedAttributeDiscription {
|
||||
name: "display".to_string(),
|
||||
namespace: Some("style".to_string()),
|
||||
},
|
||||
"flex".to_string().into(),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
let div_wrapper_id = div_wrapper.id();
|
||||
div_wrapper.add_child(pre_text_id);
|
||||
div_wrapper.add_child(highlighted_text_span_id);
|
||||
div_wrapper.add_child(post_text_id);
|
||||
|
||||
div_wrapper.add_event_listener("mousemove");
|
||||
div_wrapper.add_event_listener("mousedown");
|
||||
div_wrapper.add_event_listener("mouseup");
|
||||
div_wrapper.add_event_listener("mouseleave");
|
||||
div_wrapper.add_event_listener("mouseenter");
|
||||
root.add_event_listener("keydown");
|
||||
root.add_event_listener("focusout");
|
||||
|
||||
Self {
|
||||
pre_cursor_text: pre_text_id,
|
||||
highlighted_text: highlighted_text_id,
|
||||
post_cursor_text: post_text_id,
|
||||
div_wrapper: div_wrapper_id,
|
||||
cursor: Cursor::default(),
|
||||
text: value.unwrap_or_default(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn attributes_changed(
|
||||
&mut self,
|
||||
mut root: dioxus_native_core::real_dom::NodeMut,
|
||||
attributes: &dioxus_native_core::node_ref::AttributeMask,
|
||||
) {
|
||||
match attributes {
|
||||
AttributeMask::All => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
self.update_value_attr(&el);
|
||||
self.update_size_attr(&mut el);
|
||||
self.update_max_width_attr(&el);
|
||||
}
|
||||
let id = root.id();
|
||||
self.write_value(root.real_dom_mut(), id);
|
||||
}
|
||||
AttributeMask::Some(attrs) => {
|
||||
{
|
||||
let node_type = root.node_type_mut();
|
||||
let NodeTypeMut::Element(mut el) = node_type else {
|
||||
panic!("input must be an element")
|
||||
};
|
||||
if attrs.contains("width") || attrs.contains("height") {
|
||||
self.update_size_attr(&mut el);
|
||||
}
|
||||
if attrs.contains("maxlength") {
|
||||
self.update_max_width_attr(&el);
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
self.update_value_attr(&el);
|
||||
}
|
||||
}
|
||||
if attrs.contains("value") {
|
||||
let id = root.id();
|
||||
self.write_value(root.real_dom_mut(), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: TextLikeController + Send + Sync + Default + 'static> RinkWidget for TextLike<C> {
|
||||
fn handle_event(&mut self, event: &crate::Event, node: NodeMut) {
|
||||
match event.name {
|
||||
"keydown" => {
|
||||
if let EventData::Keyboard(data) = &event.data {
|
||||
self.handle_keydown(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
"mousemove" => {
|
||||
if let EventData::Mouse(data) = &event.data {
|
||||
self.handle_mousemove(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
"mousedown" => {
|
||||
if let EventData::Mouse(data) = &event.data {
|
||||
self.handle_mousedown(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
"mouseup" => {
|
||||
self.dragging = false;
|
||||
}
|
||||
|
||||
"mouseleave" => {
|
||||
self.dragging = false;
|
||||
}
|
||||
|
||||
"mouseenter" => {
|
||||
self.dragging = false;
|
||||
}
|
||||
|
||||
"focusout" => {
|
||||
execute!(stdout(), MoveTo(0, 1000)).unwrap();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
use super::text_like::TextLike;
|
||||
|
||||
pub(crate) type TextBox = TextLike;
|
||||
Reference in new issue
Block a user