Merge branch 'upstream' into query-system
This commit is contained in:
commit
2ca5ff96df
97 files changed
+6218
-4877
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> {
|
||||
match (self, other) {
|
||||
(FocusLevel::Unfocusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Equal),
|
||||
(FocusLevel::Unfocusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Less),
|
||||
(FocusLevel::Unfocusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Less),
|
||||
(FocusLevel::Focusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater),
|
||||
(FocusLevel::Focusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Equal),
|
||||
(FocusLevel::Focusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Greater),
|
||||
(FocusLevel::Ordered(_), FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater),
|
||||
(FocusLevel::Ordered(_), FocusLevel::Focusable) => Some(std::cmp::Ordering::Less),
|
||||
(FocusLevel::Ordered(a), FocusLevel::Ordered(b)) => a.partial_cmp(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FocusLevel {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,955 @@
|
||||
use crossterm::event::{
|
||||
Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind,
|
||||
};
|
||||
use dioxus_native_core::prelude::*;
|
||||
use dioxus_native_core::real_dom::NodeImmutable;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use dioxus_html::geometry::euclid::{Point2D, Rect, Size2D};
|
||||
use dioxus_html::geometry::{
|
||||
ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint, WheelDelta,
|
||||
};
|
||||
use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers};
|
||||
use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
|
||||
use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
|
||||
use dioxus_html::{event_bubbles, EventData, FocusData, KeyboardData, MouseData, WheelData};
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use taffy::geometry::{Point, Size};
|
||||
use taffy::{prelude::Layout, Taffy};
|
||||
|
||||
use crate::focus::{Focus, Focused};
|
||||
use crate::layout::TaffyLayout;
|
||||
use crate::{layout_to_screen_space, FocusState};
|
||||
|
||||
pub(crate) struct Event {
|
||||
pub id: NodeId,
|
||||
pub name: &'static str,
|
||||
pub data: Rc<EventData>,
|
||||
pub bubbles: bool,
|
||||
}
|
||||
|
||||
type EventCore = (&'static str, EventData);
|
||||
|
||||
const MAX_REPEAT_TIME: Duration = Duration::from_millis(100);
|
||||
|
||||
pub struct InnerInputState {
|
||||
mouse: Option<MouseData>,
|
||||
wheel: Option<WheelData>,
|
||||
last_key_pressed: Option<(KeyboardData, Instant)>,
|
||||
pub(crate) focus_state: FocusState,
|
||||
// subscribers: Vec<Rc<dyn Fn() + 'static>>,
|
||||
}
|
||||
|
||||
impl InnerInputState {
|
||||
fn create(rdom: &mut RealDom) -> Self {
|
||||
Self {
|
||||
mouse: None,
|
||||
wheel: None,
|
||||
last_key_pressed: None,
|
||||
// subscribers: Vec::new(),
|
||||
focus_state: FocusState::create(rdom),
|
||||
}
|
||||
}
|
||||
|
||||
// stores current input state and transforms events based on that state
|
||||
fn apply_event(&mut self, evt: &mut EventCore) {
|
||||
match evt.1 {
|
||||
// limitations: only two buttons may be held at once
|
||||
EventData::Mouse(ref mut m) => {
|
||||
let mut held_buttons = match &self.mouse {
|
||||
Some(previous_data) => previous_data.held_buttons(),
|
||||
None => MouseButtonSet::empty(),
|
||||
};
|
||||
|
||||
match evt.0 {
|
||||
"mousedown" => {
|
||||
held_buttons.insert(
|
||||
m.trigger_button()
|
||||
.expect("No trigger button for mousedown event"),
|
||||
);
|
||||
}
|
||||
"mouseup" => {
|
||||
held_buttons.remove(
|
||||
m.trigger_button()
|
||||
.expect("No trigger button for mouseup event"),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let new_mouse_data = MouseData::new(
|
||||
m.coordinates(),
|
||||
m.trigger_button(),
|
||||
held_buttons,
|
||||
m.modifiers(),
|
||||
);
|
||||
|
||||
self.mouse = Some(new_mouse_data.clone());
|
||||
*m = new_mouse_data;
|
||||
}
|
||||
EventData::Wheel(ref w) => self.wheel = Some(w.clone()),
|
||||
EventData::Keyboard(ref mut k) => {
|
||||
let is_repeating = self
|
||||
.last_key_pressed
|
||||
.as_ref()
|
||||
// heuristic for guessing which presses are auto-repeating. not necessarily accurate
|
||||
.filter(|(last_data, last_instant)| {
|
||||
last_data.key() == k.key() && last_instant.elapsed() < MAX_REPEAT_TIME
|
||||
})
|
||||
.is_some();
|
||||
|
||||
if is_repeating {
|
||||
*k = KeyboardData::new(k.key(), k.code(), k.location(), true, k.modifiers());
|
||||
}
|
||||
|
||||
self.last_key_pressed = Some((k.clone(), Instant::now()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
evts: &mut Vec<EventCore>,
|
||||
resolved_events: &mut Vec<Event>,
|
||||
layout: &Taffy,
|
||||
dom: &mut RealDom,
|
||||
) {
|
||||
let previous_mouse = self.mouse.clone();
|
||||
|
||||
self.wheel = None;
|
||||
|
||||
let old_focus = self.focus_state.last_focused_id;
|
||||
|
||||
evts.retain(|e| match &e.1 {
|
||||
EventData::Keyboard(k) => match k.code() {
|
||||
Code::Tab => !self
|
||||
.focus_state
|
||||
.progress(dom, !k.modifiers().contains(Modifiers::SHIFT)),
|
||||
_ => true,
|
||||
},
|
||||
_ => true,
|
||||
});
|
||||
|
||||
for e in evts.iter_mut() {
|
||||
self.apply_event(e);
|
||||
}
|
||||
|
||||
self.resolve_mouse_events(previous_mouse, resolved_events, layout, dom);
|
||||
|
||||
if old_focus != self.focus_state.last_focused_id {
|
||||
// elements with listeners will always have a element id
|
||||
if let Some(id) = self.focus_state.last_focused_id {
|
||||
resolved_events.push(Event {
|
||||
name: "focus",
|
||||
id,
|
||||
data: Rc::new(EventData::Focus(FocusData {})),
|
||||
bubbles: event_bubbles("focus"),
|
||||
});
|
||||
resolved_events.push(Event {
|
||||
name: "focusin",
|
||||
id,
|
||||
data: Rc::new(EventData::Focus(FocusData {})),
|
||||
bubbles: event_bubbles("focusin"),
|
||||
});
|
||||
}
|
||||
if let Some(id) = old_focus {
|
||||
resolved_events.push(Event {
|
||||
name: "focusout",
|
||||
id,
|
||||
data: Rc::new(EventData::Focus(FocusData {})),
|
||||
bubbles: event_bubbles("focusout"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// for s in &self.subscribers {
|
||||
// s();
|
||||
// }
|
||||
}
|
||||
|
||||
fn resolve_mouse_events(
|
||||
&mut self,
|
||||
previous_mouse: Option<MouseData>,
|
||||
resolved_events: &mut Vec<Event>,
|
||||
layout: &Taffy,
|
||||
dom: &mut RealDom,
|
||||
) {
|
||||
fn layout_contains_point(layout: &Layout, point: ScreenPoint) -> bool {
|
||||
let Point { x, y } = layout.location;
|
||||
let (x, y) = (
|
||||
layout_to_screen_space(x).round(),
|
||||
layout_to_screen_space(y).round(),
|
||||
);
|
||||
let Size { width, height } = layout.size;
|
||||
let (width, height) = (
|
||||
layout_to_screen_space(width).round(),
|
||||
layout_to_screen_space(height).round(),
|
||||
);
|
||||
|
||||
let layout_rect = Rect::new(Point2D::new(x, y), Size2D::new(width, height));
|
||||
layout_rect.contains(point.cast())
|
||||
}
|
||||
|
||||
fn try_create_event(
|
||||
name: &'static str,
|
||||
data: Rc<EventData>,
|
||||
will_bubble: &mut FxHashSet<NodeId>,
|
||||
resolved_events: &mut Vec<Event>,
|
||||
node: NodeRef,
|
||||
dom: &RealDom,
|
||||
) {
|
||||
// only trigger event if the event was not triggered already by a child
|
||||
let id = node.id();
|
||||
if will_bubble.insert(id) {
|
||||
let mut parent = Some(node);
|
||||
while let Some(current_parent) = parent {
|
||||
let parent_id = current_parent.id();
|
||||
will_bubble.insert(parent_id);
|
||||
parent = current_parent.parent_id().and_then(|id| dom.get(id));
|
||||
}
|
||||
resolved_events.push(Event {
|
||||
name,
|
||||
id,
|
||||
data,
|
||||
bubbles: event_bubbles(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_mouse_data(mouse_data: &MouseData, layout: &Layout) -> MouseData {
|
||||
let Point { x, y } = layout.location;
|
||||
let node_origin = ClientPoint::new(
|
||||
layout_to_screen_space(x).into(),
|
||||
layout_to_screen_space(y).into(),
|
||||
);
|
||||
|
||||
let new_client_coordinates = (mouse_data.client_coordinates() - node_origin)
|
||||
.to_point()
|
||||
.cast_unit();
|
||||
|
||||
let coordinates = Coordinates::new(
|
||||
mouse_data.screen_coordinates(),
|
||||
mouse_data.client_coordinates(),
|
||||
new_client_coordinates,
|
||||
mouse_data.page_coordinates(),
|
||||
);
|
||||
|
||||
MouseData::new(
|
||||
coordinates,
|
||||
mouse_data.trigger_button(),
|
||||
mouse_data.held_buttons(),
|
||||
mouse_data.modifiers(),
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(mouse_data) = &self.mouse {
|
||||
let new_pos = mouse_data.screen_coordinates();
|
||||
let old_pos = previous_mouse.as_ref().map(|m| m.screen_coordinates());
|
||||
|
||||
// a mouse button is pressed if a button was not down and is now down
|
||||
let previous_buttons = previous_mouse
|
||||
.map_or(MouseButtonSet::empty(), |previous_data| {
|
||||
previous_data.held_buttons()
|
||||
});
|
||||
let was_pressed = !(mouse_data.held_buttons() - previous_buttons).is_empty();
|
||||
|
||||
// a mouse button is released if a button was down and is now not down
|
||||
let was_released = !(previous_buttons - mouse_data.held_buttons()).is_empty();
|
||||
|
||||
let was_scrolled = self
|
||||
.wheel
|
||||
.as_ref()
|
||||
.map_or(false, |data| !data.delta().is_zero());
|
||||
let wheel_data = &self.wheel;
|
||||
|
||||
{
|
||||
// mousemove
|
||||
if old_pos != Some(new_pos) {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mousemove") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let previously_contained = old_pos
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.is_some();
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains && previously_contained {
|
||||
try_create_event(
|
||||
"mousemove",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// mouseenter
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mouseenter") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let previously_contained = old_pos
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.is_some();
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains && !previously_contained {
|
||||
try_create_event(
|
||||
"mouseenter",
|
||||
Rc::new(dioxus_html::EventData::Mouse(mouse_data.clone())),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// mouseover
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mouseover") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let previously_contained = old_pos
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.is_some();
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains && !previously_contained {
|
||||
try_create_event(
|
||||
"mouseover",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mousedown
|
||||
if was_pressed {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mousedown") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"mousedown",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// mouseup
|
||||
if was_released {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mouseup") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"mouseup",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// click
|
||||
if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("click") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"click",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// contextmenu
|
||||
if mouse_data.trigger_button() == Some(DioxusMouseButton::Secondary) && was_released
|
||||
{
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("contextmenu") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"contextmenu",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// wheel
|
||||
if let Some(w) = wheel_data {
|
||||
if was_scrolled {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("wheel") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"wheel",
|
||||
Rc::new(EventData::Wheel(w.clone())),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// mouseleave
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mouseleave") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let previously_contained = old_pos
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.is_some();
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if !currently_contains && previously_contained {
|
||||
try_create_event(
|
||||
"mouseleave",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// mouseout
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mouseout") {
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let previously_contained = old_pos
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.is_some();
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if !currently_contains && previously_contained {
|
||||
try_create_event(
|
||||
"mouseout",
|
||||
Rc::new(EventData::Mouse(prepare_mouse_data(
|
||||
mouse_data,
|
||||
&node_layout,
|
||||
))),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
dom,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update focus
|
||||
if was_released {
|
||||
let mut focus_id = None;
|
||||
dom.traverse_depth_first(|node| {
|
||||
let node_layout = layout
|
||||
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
|
||||
.unwrap();
|
||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||
|
||||
if currently_contains && node.get::<Focus>().unwrap().level.focusable() {
|
||||
focus_id = Some(node.id());
|
||||
}
|
||||
});
|
||||
if let Some(id) = focus_id {
|
||||
self.focus_state.set_focus(dom, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn subscribe(&mut self, f: Rc<dyn Fn() + 'static>) {
|
||||
// self.subscribers.push(f)
|
||||
// }
|
||||
}
|
||||
|
||||
fn get_abs_layout(node: NodeRef, dom: &RealDom, taffy: &Taffy) -> Layout {
|
||||
let mut node_layout = *taffy
|
||||
.layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
|
||||
.unwrap();
|
||||
let mut current = node;
|
||||
|
||||
while let Some(parent) = current.parent_id() {
|
||||
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
|
||||
}
|
||||
|
||||
pub struct RinkInputHandler {
|
||||
state: Rc<RefCell<InnerInputState>>,
|
||||
queued_events: Rc<RefCell<Vec<EventCore>>>,
|
||||
}
|
||||
|
||||
impl RinkInputHandler {
|
||||
/// global context that handles events
|
||||
/// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once
|
||||
pub fn create(rdom: &mut RealDom) -> (Self, impl FnMut(crossterm::event::Event)) {
|
||||
let queued_events = Rc::new(RefCell::new(Vec::new()));
|
||||
let queued_events2 = Rc::downgrade(&queued_events);
|
||||
|
||||
let regester_event = move |evt: crossterm::event::Event| {
|
||||
if let Some(evt) = get_event(evt) {
|
||||
if let Some(v) = queued_events2.upgrade() {
|
||||
(*v).borrow_mut().push(evt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let state = Rc::new(RefCell::new(InnerInputState::create(rdom)));
|
||||
|
||||
(
|
||||
Self {
|
||||
state,
|
||||
queued_events,
|
||||
},
|
||||
regester_event,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut RealDom) -> Vec<Event> {
|
||||
let mut resolved_events = Vec::new();
|
||||
|
||||
(*self.state).borrow_mut().update(
|
||||
&mut (*self.queued_events).borrow_mut(),
|
||||
&mut resolved_events,
|
||||
layout,
|
||||
dom,
|
||||
);
|
||||
|
||||
let events = self
|
||||
.queued_events
|
||||
.replace(Vec::new())
|
||||
.into_iter()
|
||||
// these events were added in the update stage
|
||||
.filter(|e| {
|
||||
![
|
||||
"mouseenter",
|
||||
"mouseover",
|
||||
"mouseleave",
|
||||
"mouseout",
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"drag",
|
||||
"wheel",
|
||||
"click",
|
||||
"contextmenu",
|
||||
]
|
||||
.contains(&e.0)
|
||||
})
|
||||
.map(|evt| (evt.0, evt.1));
|
||||
|
||||
let mut hm: FxHashMap<&'static str, Vec<Rc<EventData>>> = FxHashMap::default();
|
||||
for (event, data) in events {
|
||||
if let Some(v) = hm.get_mut(event) {
|
||||
v.push(Rc::new(data));
|
||||
} else {
|
||||
hm.insert(event, vec![Rc::new(data)]);
|
||||
}
|
||||
}
|
||||
for (event, datas) in hm {
|
||||
for node in dom.get_listening_sorted(event) {
|
||||
for data in &datas {
|
||||
let focused = node.get::<Focused>();
|
||||
if focused.is_some() && focused.unwrap().0 {
|
||||
resolved_events.push(Event {
|
||||
name: event,
|
||||
id: node.id(),
|
||||
data: data.clone(),
|
||||
bubbles: event_bubbles(event),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolved_events
|
||||
}
|
||||
|
||||
pub(crate) fn state(&self) -> RefMut<InnerInputState> {
|
||||
self.state.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
// translate crossterm events into dioxus events
|
||||
fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
|
||||
let (name, data): (&str, EventData) = match evt {
|
||||
TermEvent::Key(k) => ("keydown", translate_key_event(k)?),
|
||||
TermEvent::Mouse(m) => {
|
||||
let (x, y) = (m.column.into(), m.row.into());
|
||||
let alt = m.modifiers.contains(KeyModifiers::ALT);
|
||||
let shift = m.modifiers.contains(KeyModifiers::SHIFT);
|
||||
let ctrl = m.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let meta = false;
|
||||
|
||||
let get_mouse_data = |crossterm_button: Option<MouseButton>| {
|
||||
let button = crossterm_button.map(|b| match b {
|
||||
MouseButton::Left => DioxusMouseButton::Primary,
|
||||
MouseButton::Right => DioxusMouseButton::Secondary,
|
||||
MouseButton::Middle => DioxusMouseButton::Auxiliary,
|
||||
});
|
||||
|
||||
// from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
|
||||
|
||||
// The `page` and `screen` coordinates are inconsistent with the MDN definition, as they are relative to the viewport (client), not the target element/page/screen, respectively.
|
||||
// todo?
|
||||
// But then, MDN defines them in terms of pixels, yet crossterm provides only row/column, and it might not be possible to get pixels. So we can't get 100% consistency anyway.
|
||||
let coordinates = Coordinates::new(
|
||||
ScreenPoint::new(x, y),
|
||||
ClientPoint::new(x, y),
|
||||
// offset x/y are set when the origin of the event is assigned to an element
|
||||
ElementPoint::new(0., 0.),
|
||||
PagePoint::new(x, y),
|
||||
);
|
||||
|
||||
let mut modifiers = Modifiers::empty();
|
||||
if shift {
|
||||
modifiers.insert(Modifiers::SHIFT);
|
||||
}
|
||||
if ctrl {
|
||||
modifiers.insert(Modifiers::CONTROL);
|
||||
}
|
||||
if meta {
|
||||
modifiers.insert(Modifiers::META);
|
||||
}
|
||||
if alt {
|
||||
modifiers.insert(Modifiers::ALT);
|
||||
}
|
||||
|
||||
// held mouse buttons get set later by maintaining state, as crossterm does not provide them
|
||||
EventData::Mouse(MouseData::new(
|
||||
coordinates,
|
||||
button,
|
||||
DioxusMouseButtons::empty(),
|
||||
modifiers,
|
||||
))
|
||||
};
|
||||
|
||||
let get_wheel_data = |up| {
|
||||
let y = if up { -1.0 } else { 1.0 };
|
||||
EventData::Wheel(WheelData::new(WheelDelta::lines(0., y, 0.)))
|
||||
};
|
||||
|
||||
match m.kind {
|
||||
MouseEventKind::Down(b) => ("mousedown", get_mouse_data(Some(b))),
|
||||
MouseEventKind::Up(b) => ("mouseup", get_mouse_data(Some(b))),
|
||||
MouseEventKind::Drag(b) => ("drag", get_mouse_data(Some(b))),
|
||||
MouseEventKind::Moved => ("mousemove", get_mouse_data(None)),
|
||||
MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)),
|
||||
MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)),
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some((name, data))
|
||||
}
|
||||
|
||||
fn translate_key_event(event: crossterm::event::KeyEvent) -> Option<EventData> {
|
||||
let key = key_from_crossterm_key_code(event.code);
|
||||
// crossterm does not provide code. we make a guess as to which key might have been pressed
|
||||
// this is probably garbage if the user has a custom keyboard layout
|
||||
let code = guess_code_from_crossterm_key_code(event.code)?;
|
||||
let modifiers = modifiers_from_crossterm_modifiers(event.modifiers);
|
||||
|
||||
Some(EventData::Keyboard(KeyboardData::new(
|
||||
key,
|
||||
code,
|
||||
Location::Standard,
|
||||
false,
|
||||
modifiers,
|
||||
)))
|
||||
}
|
||||
|
||||
/// The crossterm key_code nicely represents the meaning of the key and we can mostly convert it without any issues
|
||||
///
|
||||
/// Exceptions:
|
||||
/// BackTab is converted to Key::Tab, and Null is converted to Key::Unidentified
|
||||
fn key_from_crossterm_key_code(key_code: TermKeyCode) -> Key {
|
||||
match key_code {
|
||||
TermKeyCode::Backspace => Key::Backspace,
|
||||
TermKeyCode::Enter => Key::Enter,
|
||||
TermKeyCode::Left => Key::ArrowLeft,
|
||||
TermKeyCode::Right => Key::ArrowRight,
|
||||
TermKeyCode::Up => Key::ArrowUp,
|
||||
TermKeyCode::Down => Key::ArrowDown,
|
||||
TermKeyCode::Home => Key::Home,
|
||||
TermKeyCode::End => Key::End,
|
||||
TermKeyCode::PageUp => Key::PageUp,
|
||||
TermKeyCode::PageDown => Key::PageDown,
|
||||
TermKeyCode::Tab => Key::Tab,
|
||||
// ? no corresponding Key
|
||||
TermKeyCode::BackTab => Key::Tab,
|
||||
TermKeyCode::Delete => Key::Delete,
|
||||
TermKeyCode::Insert => Key::Insert,
|
||||
TermKeyCode::F(1) => Key::F1,
|
||||
TermKeyCode::F(2) => Key::F2,
|
||||
TermKeyCode::F(3) => Key::F3,
|
||||
TermKeyCode::F(4) => Key::F4,
|
||||
TermKeyCode::F(5) => Key::F5,
|
||||
TermKeyCode::F(6) => Key::F6,
|
||||
TermKeyCode::F(7) => Key::F7,
|
||||
TermKeyCode::F(8) => Key::F8,
|
||||
TermKeyCode::F(9) => Key::F9,
|
||||
TermKeyCode::F(10) => Key::F10,
|
||||
TermKeyCode::F(11) => Key::F11,
|
||||
TermKeyCode::F(12) => Key::F12,
|
||||
TermKeyCode::F(13) => Key::F13,
|
||||
TermKeyCode::F(14) => Key::F14,
|
||||
TermKeyCode::F(15) => Key::F15,
|
||||
TermKeyCode::F(16) => Key::F16,
|
||||
TermKeyCode::F(17) => Key::F17,
|
||||
TermKeyCode::F(18) => Key::F18,
|
||||
TermKeyCode::F(19) => Key::F19,
|
||||
TermKeyCode::F(20) => Key::F20,
|
||||
TermKeyCode::F(21) => Key::F21,
|
||||
TermKeyCode::F(22) => Key::F22,
|
||||
TermKeyCode::F(23) => Key::F23,
|
||||
TermKeyCode::F(24) => Key::F24,
|
||||
TermKeyCode::F(other) => {
|
||||
panic!("Unexpected function key: {other:?}")
|
||||
}
|
||||
TermKeyCode::Char(c) => Key::Character(c.to_string()),
|
||||
TermKeyCode::Null => Key::Unidentified,
|
||||
TermKeyCode::Esc => Key::Escape,
|
||||
}
|
||||
}
|
||||
|
||||
// Crossterm does not provide a way to get the `code` (physical key on keyboard)
|
||||
// So we make a guess based on their `key_code`, but this is probably going to break on anything other than a very standard european keyboard
|
||||
// It may look fine, but it's a horrible hack. But there's nothing better we can do.
|
||||
fn guess_code_from_crossterm_key_code(key_code: TermKeyCode) -> Option<Code> {
|
||||
let code = match key_code {
|
||||
TermKeyCode::Backspace => Code::Backspace,
|
||||
TermKeyCode::Enter => Code::Enter,
|
||||
TermKeyCode::Left => Code::ArrowLeft,
|
||||
TermKeyCode::Right => Code::ArrowRight,
|
||||
TermKeyCode::Up => Code::ArrowUp,
|
||||
TermKeyCode::Down => Code::ArrowDown,
|
||||
TermKeyCode::Home => Code::Home,
|
||||
TermKeyCode::End => Code::End,
|
||||
TermKeyCode::PageUp => Code::PageUp,
|
||||
TermKeyCode::PageDown => Code::PageDown,
|
||||
TermKeyCode::Tab => Code::Tab,
|
||||
// ? Apparently you get BackTab by pressing Tab
|
||||
TermKeyCode::BackTab => Code::Tab,
|
||||
TermKeyCode::Delete => Code::Delete,
|
||||
TermKeyCode::Insert => Code::Insert,
|
||||
TermKeyCode::F(1) => Code::F1,
|
||||
TermKeyCode::F(2) => Code::F2,
|
||||
TermKeyCode::F(3) => Code::F3,
|
||||
TermKeyCode::F(4) => Code::F4,
|
||||
TermKeyCode::F(5) => Code::F5,
|
||||
TermKeyCode::F(6) => Code::F6,
|
||||
TermKeyCode::F(7) => Code::F7,
|
||||
TermKeyCode::F(8) => Code::F8,
|
||||
TermKeyCode::F(9) => Code::F9,
|
||||
TermKeyCode::F(10) => Code::F10,
|
||||
TermKeyCode::F(11) => Code::F11,
|
||||
TermKeyCode::F(12) => Code::F12,
|
||||
TermKeyCode::F(13) => Code::F13,
|
||||
TermKeyCode::F(14) => Code::F14,
|
||||
TermKeyCode::F(15) => Code::F15,
|
||||
TermKeyCode::F(16) => Code::F16,
|
||||
TermKeyCode::F(17) => Code::F17,
|
||||
TermKeyCode::F(18) => Code::F18,
|
||||
TermKeyCode::F(19) => Code::F19,
|
||||
TermKeyCode::F(20) => Code::F20,
|
||||
TermKeyCode::F(21) => Code::F21,
|
||||
TermKeyCode::F(22) => Code::F22,
|
||||
TermKeyCode::F(23) => Code::F23,
|
||||
TermKeyCode::F(24) => Code::F24,
|
||||
TermKeyCode::F(other) => {
|
||||
panic!("Unexpected function key: {other:?}")
|
||||
}
|
||||
// this is a horrible way for crossterm to represent keys but we have to deal with it
|
||||
TermKeyCode::Char(c) => match c {
|
||||
'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() {
|
||||
'A' => Code::KeyA,
|
||||
'B' => Code::KeyB,
|
||||
'C' => Code::KeyC,
|
||||
'D' => Code::KeyD,
|
||||
'E' => Code::KeyE,
|
||||
'F' => Code::KeyF,
|
||||
'G' => Code::KeyG,
|
||||
'H' => Code::KeyH,
|
||||
'I' => Code::KeyI,
|
||||
'J' => Code::KeyJ,
|
||||
'K' => Code::KeyK,
|
||||
'L' => Code::KeyL,
|
||||
'M' => Code::KeyM,
|
||||
'N' => Code::KeyN,
|
||||
'O' => Code::KeyO,
|
||||
'P' => Code::KeyP,
|
||||
'Q' => Code::KeyQ,
|
||||
'R' => Code::KeyR,
|
||||
'S' => Code::KeyS,
|
||||
'T' => Code::KeyT,
|
||||
'U' => Code::KeyU,
|
||||
'V' => Code::KeyV,
|
||||
'W' => Code::KeyW,
|
||||
'X' => Code::KeyX,
|
||||
'Y' => Code::KeyY,
|
||||
'Z' => Code::KeyZ,
|
||||
_ => unreachable!("Exhaustively checked all characters in range A..Z"),
|
||||
},
|
||||
' ' => Code::Space,
|
||||
'[' | '{' => Code::BracketLeft,
|
||||
']' | '}' => Code::BracketRight,
|
||||
';' => Code::Semicolon,
|
||||
':' => Code::Semicolon,
|
||||
',' => Code::Comma,
|
||||
'<' => Code::Comma,
|
||||
'.' => Code::Period,
|
||||
'>' => Code::Period,
|
||||
'1' => Code::Digit1,
|
||||
'2' => Code::Digit2,
|
||||
'3' => Code::Digit3,
|
||||
'4' => Code::Digit4,
|
||||
'5' => Code::Digit5,
|
||||
'6' => Code::Digit6,
|
||||
'7' => Code::Digit7,
|
||||
'8' => Code::Digit8,
|
||||
'9' => Code::Digit9,
|
||||
'0' => Code::Digit0,
|
||||
'!' => Code::Digit1,
|
||||
'@' => Code::Digit2,
|
||||
'#' => Code::Digit3,
|
||||
'$' => Code::Digit4,
|
||||
'%' => Code::Digit5,
|
||||
'^' => Code::Digit6,
|
||||
'&' => Code::Digit7,
|
||||
'*' => Code::Digit8,
|
||||
'(' => Code::Digit9,
|
||||
')' => Code::Digit0,
|
||||
// numpad characters are ambiguous; we don't know which key was really pressed
|
||||
// it could be also:
|
||||
// '*' => Code::Multiply,
|
||||
// '/' => Code::Divide,
|
||||
// '-' => Code::Subtract,
|
||||
// '+' => Code::Add,
|
||||
'+' => Code::Equal,
|
||||
'-' | '_' => Code::Minus,
|
||||
'\'' => Code::Quote,
|
||||
'"' => Code::Quote,
|
||||
'\\' => Code::Backslash,
|
||||
'|' => Code::Backslash,
|
||||
'/' => Code::Slash,
|
||||
'?' => Code::Slash,
|
||||
'=' => Code::Equal,
|
||||
'`' => Code::Backquote,
|
||||
'~' => Code::Backquote,
|
||||
_ => return None,
|
||||
},
|
||||
TermKeyCode::Null => return None,
|
||||
TermKeyCode::Esc => Code::Escape,
|
||||
};
|
||||
|
||||
Some(code)
|
||||
}
|
||||
|
||||
fn modifiers_from_crossterm_modifiers(src: KeyModifiers) -> Modifiers {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
|
||||
if src.contains(KeyModifiers::SHIFT) {
|
||||
modifiers.insert(Modifiers::SHIFT);
|
||||
}
|
||||
|
||||
if src.contains(KeyModifiers::ALT) {
|
||||
modifiers.insert(Modifiers::ALT);
|
||||
}
|
||||
|
||||
if src.contains(KeyModifiers::CONTROL) {
|
||||
modifiers.insert(Modifiers::CONTROL);
|
||||
}
|
||||
|
||||
modifiers
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
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"),
|
||||
}
|
||||
}
|
||||
pub fn ok(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Initialized(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
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).unwrap();
|
||||
}
|
||||
} else {
|
||||
self.node = PossiblyUninitalized::Initialized(taffy.new_leaf(style).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 let Some(text) = value.as_text() {
|
||||
apply_layout_attributes_cfg(
|
||||
&attribute.name,
|
||||
text,
|
||||
&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_dimention(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,
|
||||
Dimension::Undefined => Dimension::Undefined,
|
||||
}
|
||||
}
|
||||
let style = Style {
|
||||
position: Rect {
|
||||
left: scale_dimention(style.position.left),
|
||||
right: scale_dimention(style.position.right),
|
||||
top: scale_dimention(style.position.top),
|
||||
bottom: scale_dimention(style.position.bottom),
|
||||
},
|
||||
margin: Rect {
|
||||
left: scale_dimention(style.margin.left),
|
||||
right: scale_dimention(style.margin.right),
|
||||
top: scale_dimention(style.margin.top),
|
||||
bottom: scale_dimention(style.margin.bottom),
|
||||
},
|
||||
padding: Rect {
|
||||
left: scale_dimention(style.padding.left),
|
||||
right: scale_dimention(style.padding.right),
|
||||
top: scale_dimention(style.padding.top),
|
||||
bottom: scale_dimention(style.padding.bottom),
|
||||
},
|
||||
border: Rect {
|
||||
left: scale_dimention(style.border.left),
|
||||
right: scale_dimention(style.border.right),
|
||||
top: scale_dimention(style.border.top),
|
||||
bottom: scale_dimention(style.border.bottom),
|
||||
},
|
||||
gap: Size {
|
||||
width: scale_dimention(style.gap.width),
|
||||
height: scale_dimention(style.gap.height),
|
||||
},
|
||||
flex_basis: scale_dimention(style.flex_basis),
|
||||
size: Size {
|
||||
width: scale_dimention(style.size.width),
|
||||
height: scale_dimention(style.size.height),
|
||||
},
|
||||
min_size: Size {
|
||||
width: scale_dimention(style.min_size.width),
|
||||
height: scale_dimention(style.min_size.height),
|
||||
},
|
||||
max_size: Size {
|
||||
width: scale_dimention(style.max_size.width),
|
||||
height: scale_dimention(style.max_size.height),
|
||||
},
|
||||
..style
|
||||
};
|
||||
|
||||
if let PossiblyUninitalized::Initialized(n) = self.node {
|
||||
if self.style != style {
|
||||
taffy.set_style(n, 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(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,311 @@
|
||||
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_html::EventData;
|
||||
use dioxus_native_core::prelude::*;
|
||||
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 std::{io, time::Duration};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::{rc::Rc, sync::RwLock};
|
||||
use style_attributes::StyleModifier;
|
||||
use taffy::Taffy;
|
||||
pub use taffy::{geometry::Point, prelude::*};
|
||||
use tokio::select;
|
||||
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
||||
|
||||
mod config;
|
||||
mod focus;
|
||||
mod hooks;
|
||||
mod layout;
|
||||
pub mod prelude;
|
||||
mod prevent_default;
|
||||
pub mod query;
|
||||
mod render;
|
||||
mod style;
|
||||
mod style_attributes;
|
||||
mod widget;
|
||||
|
||||
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(),
|
||||
]);
|
||||
|
||||
let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
|
||||
|
||||
// Setup input handling
|
||||
let (event_tx, mut event_reciever) = unbounded();
|
||||
let event_tx_clone = event_tx.clone();
|
||||
if !cfg.headless {
|
||||
std::thread::spawn(move || {
|
||||
let tick_rate = Duration::from_millis(1000);
|
||||
loop {
|
||||
if crossterm::event::poll(tick_rate).unwrap() {
|
||||
let evt = crossterm::event::read().unwrap();
|
||||
if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(async {
|
||||
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: 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();
|
||||
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(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
},
|
||||
&mut taffy.lock().expect("taffy lock poisoned"),
|
||||
&rdom,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let wait = renderer.poll_async();
|
||||
|
||||
pin_mut!(wait);
|
||||
|
||||
select! {
|
||||
_ = wait => {
|
||||
|
||||
},
|
||||
evt = 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,
|
||||
TermEvent::Mouse(_) => {}
|
||||
},
|
||||
InputEvent::Close => break,
|
||||
};
|
||||
|
||||
if let InputEvent::UserInput(evt) = evt.unwrap() {
|
||||
register_event(evt);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
{
|
||||
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 {
|
||||
renderer.handle_event(&rdom, e.id, e.name, 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 = ()> + '_>>;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "dioxus-bindings")]
|
||||
pub use crate::widgets::*;
|
||||
@@ -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,105 @@
|
||||
use std::sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard};
|
||||
|
||||
use dioxus_native_core::prelude::*;
|
||||
use taffy::{
|
||||
geometry::Point,
|
||||
prelude::{Layout, Size},
|
||||
Taffy,
|
||||
};
|
||||
|
||||
use crate::{layout::TaffyLayout, 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(cx: Scope) -> Element {
|
||||
/// let hue = use_state(cx, || 0.0);
|
||||
/// let brightness = use_state(cx, || 0.0);
|
||||
/// let tui_query: Query = cx.consume_context().unwrap();
|
||||
/// cx.render(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)]
|
||||
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> {
|
||||
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> {
|
||||
let layout = self
|
||||
.stretch
|
||||
.layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
|
||||
.ok();
|
||||
layout.map(|layout| Layout {
|
||||
order: layout.order,
|
||||
size: layout.size.map(layout_to_screen_space),
|
||||
location: Point {
|
||||
x: layout_to_screen_space(layout.location.x),
|
||||
y: layout_to_screen_space(layout.location.y),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
use dioxus_native_core::prelude::*;
|
||||
use std::io::Stdout;
|
||||
use taffy::{
|
||||
geometry::Point,
|
||||
prelude::{Dimension, Layout, Size},
|
||||
Taffy,
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
|
||||
|
||||
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 tui::Frame<CrosstermBackend<Stdout>>,
|
||||
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);
|
||||
}
|
||||
|
||||
for c in node.children() {
|
||||
render_vnode(frame, layout, c, cfg, location);
|
||||
}
|
||||
}
|
||||
NodeType::Placeholder => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
impl RinkWidget for NodeRef<'_> {
|
||||
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
|
||||
use tui::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,451 @@
|
||||
use std::{num::ParseFloatError, str::FromStr};
|
||||
|
||||
use tui::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]
|
||||
}
|
||||
// rink 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 {
|
||||
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<tui::symbols::line::Set> {
|
||||
use tui::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 tui::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 tui::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 tui::{
|
||||
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 mut 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user