123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- use anyhow::Result;
- use crossterm::{
- event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
- execute,
- terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
- };
- use futures_channel::mpsc::unbounded;
- use dioxus_core::*;
- use dioxus_native_core::{real_dom::RealDom, NodeId, SendAnyMap, FxDashSet, NodeMask};
- use focus::FocusState;
- use futures::{
- channel::mpsc::{UnboundedReceiver, UnboundedSender},
- pin_mut, StreamExt,
- };
- use query::Query;
- use std::{cell::RefCell, sync::{Mutex, Arc}};
- use std::rc::Rc;
- use std::{io, time::Duration};
- use taffy::{Taffy};
- pub use taffy::{geometry::Point, prelude::*};
- use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
- mod config;
- mod focus;
- mod hooks;
- mod layout;
- mod node;
- pub mod query;
- mod render;
- mod style;
- mod style_attributes;
- mod widget;
- pub use config::*;
- pub use hooks::*;
- pub(crate) use node::*;
- // 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 layout_to_screen_space(layout: f32) -> f32{
- layout / 10.0
- }
- #[derive(Clone)]
- pub struct TuiContext {
- tx: UnboundedSender<InputEvent>,
- }
- impl TuiContext {
- 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 launch(app: Component<()>) {
- launch_cfg(app, Config::default())
- }
- pub fn launch_cfg(app: Component<()>, cfg: Config) {
- let mut dom = VirtualDom::new(app);
- let (handler, state, register_event) = RinkInputHandler::new();
- // Setup input handling
- let (event_tx, event_rx) = 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 cx = dom.base_scope();
- let rdom = Rc::new(RefCell::new(RealDom::new()));
- let taffy = Arc::new(Mutex::new(Taffy::new()));
- cx.provide_context(state);
- cx.provide_context(TuiContext { tx: event_tx_clone });
- cx.provide_context(Query {
- rdom: rdom.clone(),
- stretch: taffy.clone(),
- });
- {
- let mut rdom = rdom.borrow_mut();
- let mutations = dom.rebuild();
- let (to_update,_) = rdom.apply_mutations(mutations);
- let mut any_map = SendAnyMap::new();
- any_map.insert(taffy.clone());
- let _to_rerender = rdom.update_state(to_update, any_map);
- }
- render_vdom(
- &mut dom,
- event_rx,
- handler,
- cfg,
- rdom,
- taffy,
- register_event,
- )
- .unwrap();
- }
- fn render_vdom(
- vdom: &mut VirtualDom,
- mut event_reciever: UnboundedReceiver<InputEvent>,
- handler: RinkInputHandler,
- cfg: Config,
- rdom: Rc<RefCell<TuiDom>>,
- taffy: Arc<Mutex<Taffy>>,
- mut register_event: impl FnMut(crossterm::event::Event),
- ) -> Result<()> {
- 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).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(NodeId(0));
- 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: &TuiDom) {
- let width = screen_to_layout_space(dims.width );
- let height = screen_to_layout_space(dims.height);
- let root_node = rdom[NodeId(0)].state.layout.node.unwrap();
-
- // the root node fills the entire area
-
- let mut style=*taffy.style(root_node).unwrap();
- style.size=Size {
- width: Dimension::Points(width ),
- height: Dimension::Points(height ),
- };
- 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 {
- terminal.draw(|frame| {
- let rdom = rdom.borrow();
- 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[NodeId(0)];
- render::render_vnode(
- frame,
- &*taffy,
- &rdom,
- root,
- cfg,
- Point::ZERO,
- );
- })?;
- } else {
- let rdom = rdom.borrow();
- resize(
- Rect {
- x: 0,
- y: 0,
- width: 1000,
- height: 1000,
- },
- &mut taffy.lock().expect("taffy lock poisoned"),
- &rdom,
- );
- }
- }
- use futures::future::{select, Either};
- {
- let wait = vdom.wait_for_work();
- pin_mut!(wait);
- match select(wait, event_reciever.next()).await {
- Either::Left((_a, _b)) => {
- //
- }
- Either::Right((evt, _o)) => {
- 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 = {
- let mut rdom = rdom.borrow_mut();
- handler.get_events(&taffy.lock().expect("taffy lock poisoned"), &mut rdom)
- };
- {
- updated |= handler.state().focus_state.clean();
- }
- for e in evts {
- vdom.handle_event(e.name, e.data, e.id, e.bubbles)
- }
- let mut rdom = rdom.borrow_mut();
- let mutations = vdom.render_immediate();
- handler.prune(&mutations, &rdom);
- // updates the dom's nodes
- let (to_update, dirty) = rdom.apply_mutations(mutations);
- // update the style and layout
- let mut any_map = SendAnyMap::new();
- any_map.insert(taffy.clone());
- to_rerender = rdom.update_state(to_update, any_map);
- for (id, mask) in dirty {
- if mask.overlaps(&NodeMask::new().with_text()) {
- 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)]
- enum InputEvent {
- UserInput(TermEvent),
- Close,
- }
|