123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- #![doc = include_str!("../README.md")]
- #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
- #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
- use crate::focus::Focus;
- use anyhow::Result;
- use crossterm::{
- cursor::{MoveTo, RestorePosition, SavePosition, Show},
- event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
- execute,
- terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
- };
- use dioxus_native_core::{prelude::*, tree::TreeRef};
- use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap};
- use focus::FocusState;
- use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
- use futures_channel::mpsc::unbounded;
- use layout::TaffyLayout;
- use prevent_default::PreventDefault;
- use ratatui::{backend::CrosstermBackend, Terminal};
- use std::{io, time::Duration};
- use std::{
- pin::Pin,
- sync::{Arc, Mutex},
- };
- use std::{rc::Rc, sync::RwLock};
- use style_attributes::StyleModifier;
- pub use taffy::{geometry::Point, prelude::*};
- use tokio::select;
- use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
- mod config;
- mod focus;
- mod hooks;
- mod layout;
- mod prevent_default;
- pub mod query;
- mod render;
- mod style;
- mod style_attributes;
- mod widget;
- mod widgets;
- pub use config::*;
- pub use hooks::*;
- pub use query::Query;
- // the layout space has a multiplier of 10 to minimize rounding errors
- pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
- screen as f32 * 10.0
- }
- pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
- screen * 10.0
- }
- pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
- layout / 10.0
- }
- #[derive(Clone)]
- pub struct TuiContext {
- tx: UnboundedSender<InputEvent>,
- }
- impl TuiContext {
- pub fn new(tx: UnboundedSender<InputEvent>) -> Self {
- Self { tx }
- }
- pub fn quit(&self) {
- // panic!("ack")
- self.tx.unbounded_send(InputEvent::Close).unwrap();
- }
- pub fn inject_event(&self, event: crossterm::event::Event) {
- self.tx
- .unbounded_send(InputEvent::UserInput(event))
- .unwrap();
- }
- }
- pub fn render<R: Driver>(
- cfg: Config,
- create_renderer: impl FnOnce(
- &Arc<RwLock<RealDom>>,
- &Arc<Mutex<Taffy>>,
- UnboundedSender<InputEvent>,
- ) -> R,
- ) -> Result<()> {
- let mut rdom = RealDom::new([
- TaffyLayout::to_type_erased(),
- Focus::to_type_erased(),
- StyleModifier::to_type_erased(),
- PreventDefault::to_type_erased(),
- ]);
- // Setup input handling
- // The event channel for fully resolved events
- let (event_tx, mut event_reciever) = unbounded();
- // The event channel for raw terminal events
- let (raw_event_tx, mut raw_event_reciever) = unbounded();
- let event_tx_clone = raw_event_tx.clone();
- if !cfg.headless {
- std::thread::spawn(move || {
- // Timeout after 10ms when waiting for events
- let tick_rate = Duration::from_millis(10);
- loop {
- if crossterm::event::poll(tick_rate).unwrap() {
- let evt = crossterm::event::read().unwrap();
- if raw_event_tx
- .unbounded_send(InputEvent::UserInput(evt))
- .is_err()
- {
- break;
- }
- }
- }
- });
- }
- register_widgets(&mut rdom, event_tx);
- let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
- let rdom = Arc::new(RwLock::new(rdom));
- let taffy = Arc::new(Mutex::new(Taffy::new()));
- let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone);
- // insert the query engine into the rdom
- let query_engine = Query::new(rdom.clone(), taffy.clone());
- {
- let mut rdom = rdom.write().unwrap();
- rdom.raw_world_mut().add_unique(query_engine);
- }
- tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()?
- .block_on(async {
- {
- renderer.update(&rdom);
- let mut any_map = SendAnyMap::new();
- any_map.insert(taffy.clone());
- let mut rdom = rdom.write().unwrap();
- let _ = rdom.update_state(any_map);
- }
- let mut terminal = (!cfg.headless).then(|| {
- enable_raw_mode().unwrap();
- let mut stdout = std::io::stdout();
- execute!(
- stdout,
- EnterAlternateScreen,
- EnableMouseCapture,
- MoveTo(0, 1000)
- )
- .unwrap();
- let backend = CrosstermBackend::new(io::stdout());
- Terminal::new(backend).unwrap()
- });
- if let Some(terminal) = &mut terminal {
- terminal.clear().unwrap();
- }
- let mut to_rerender = FxDashSet::default();
- to_rerender.insert(rdom.read().unwrap().root_id());
- let mut updated = true;
- loop {
- /*
- -> render the nodes in the right place with tui/crossterm
- -> wait for changes
- -> resolve events
- -> lazily update the layout and style based on nodes changed
- use simd to compare lines for diffing?
- todo: lazy re-rendering
- */
- if !to_rerender.is_empty() || updated {
- updated = false;
- fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
- let width = screen_to_layout_space(dims.width);
- let height = screen_to_layout_space(dims.height);
- let root_node = rdom
- .get(rdom.root_id())
- .unwrap()
- .get::<TaffyLayout>()
- .unwrap()
- .node
- .unwrap();
- // the root node fills the entire area
- let mut style = taffy.style(root_node).unwrap().clone();
- let new_size = Size {
- width: Dimension::Points(width),
- height: Dimension::Points(height),
- };
- if style.size != new_size {
- style.size = new_size;
- taffy.set_style(root_node, style).unwrap();
- }
- let size = Size {
- width: AvailableSpace::Definite(width),
- height: AvailableSpace::Definite(height),
- };
- taffy.compute_layout(root_node, size).unwrap();
- }
- if let Some(terminal) = &mut terminal {
- execute!(terminal.backend_mut(), SavePosition).unwrap();
- terminal.draw(|frame| {
- let rdom = rdom.write().unwrap();
- let mut taffy = taffy.lock().expect("taffy lock poisoned");
- // size is guaranteed to not change when rendering
- resize(frame.size(), &mut taffy, &rdom);
- let root = rdom.get(rdom.root_id()).unwrap();
- render::render_vnode(frame, &taffy, root, cfg, Point::ZERO);
- })?;
- execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
- } else {
- let rdom = rdom.read().unwrap();
- resize(
- ratatui::layout::Rect {
- x: 0,
- y: 0,
- width: 1000,
- height: 1000,
- },
- &mut taffy.lock().expect("taffy lock poisoned"),
- &rdom,
- );
- }
- }
- let mut event_recieved = None;
- {
- let wait = renderer.poll_async();
- pin_mut!(wait);
- select! {
- _ = wait => {
- },
- evt = raw_event_reciever.next() => {
- match evt.as_ref().unwrap() {
- InputEvent::UserInput(event) => match event {
- TermEvent::Key(key) => {
- if matches!(key.code, KeyCode::Char('C' | 'c'))
- && key.modifiers.contains(KeyModifiers::CONTROL)
- && cfg.ctrl_c_quit
- {
- break;
- }
- }
- TermEvent::Resize(_, _) => updated = true,
- _ => {}
- },
- InputEvent::Close => break,
- };
- if let InputEvent::UserInput(evt) = evt.unwrap() {
- register_event(evt);
- }
- },
- Some(evt) = event_reciever.next() => {
- event_recieved = Some(evt);
- }
- }
- }
- {
- if let Some(evt) = event_recieved {
- renderer.handle_event(
- &rdom,
- evt.id,
- evt.name,
- Rc::new(evt.data),
- evt.bubbles,
- );
- }
- {
- let evts = handler.get_events(
- &taffy.lock().expect("taffy lock poisoned"),
- &mut rdom.write().unwrap(),
- );
- updated |= handler.state().focus_state.clean();
- for e in evts {
- bubble_event_to_widgets(&mut rdom.write().unwrap(), &e);
- renderer.handle_event(&rdom, e.id, e.name, Rc::new(e.data), e.bubbles);
- }
- }
- // updates the dom's nodes
- renderer.update(&rdom);
- // update the style and layout
- let mut rdom = rdom.write().unwrap();
- let mut any_map = SendAnyMap::new();
- any_map.insert(taffy.clone());
- let (new_to_rerender, dirty) = rdom.update_state(any_map);
- to_rerender = new_to_rerender;
- let text_mask = NodeMaskBuilder::new().with_text().build();
- for (id, mask) in dirty {
- if mask.overlaps(&text_mask) {
- to_rerender.insert(id);
- }
- }
- }
- }
- if let Some(terminal) = &mut terminal {
- disable_raw_mode()?;
- execute!(
- terminal.backend_mut(),
- LeaveAlternateScreen,
- DisableMouseCapture
- )?;
- terminal.show_cursor()?;
- }
- Ok(())
- })
- }
- #[derive(Debug)]
- pub enum InputEvent {
- UserInput(TermEvent),
- Close,
- }
- pub trait Driver {
- fn update(&mut self, rdom: &Arc<RwLock<RealDom>>);
- fn handle_event(
- &mut self,
- rdom: &Arc<RwLock<RealDom>>,
- id: NodeId,
- event: &str,
- value: Rc<EventData>,
- bubbles: bool,
- );
- fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
- }
- /// Before sending the event to drivers, we need to bubble it up the tree to any widgets that are listening
- fn bubble_event_to_widgets(rdom: &mut RealDom, event: &Event) {
- let id = event.id;
- let mut node = Some(id);
- while let Some(node_id) = node {
- let parent_id = {
- let tree = rdom.tree_ref();
- tree.parent_id_advanced(node_id, true)
- };
- {
- // println!("@ bubbling event to node {:?}", node_id);
- let mut node_mut = rdom.get_mut(node_id).unwrap();
- if let Some(mut widget) = node_mut
- .get_mut::<RinkWidgetTraitObject>()
- .map(|w| w.clone())
- {
- widget.handle_event(event, node_mut)
- }
- }
- if !event.bubbles {
- // println!("event does not bubble");
- break;
- }
- node = parent_id;
- }
- }
- pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout {
- let mut node_layout = *taffy
- .layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
- .unwrap();
- let mut current = node;
- let dom = node.real_dom();
- let tree = dom.tree_ref();
- while let Some(parent) = tree.parent_id_advanced(current.id(), true) {
- let parent = dom.get(parent).unwrap();
- current = parent;
- let parent_layout = taffy
- .layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
- .unwrap();
- node_layout.location.x += parent_layout.location.x;
- node_layout.location.y += parent_layout.location.y;
- }
- node_layout
- }
|