1
0

lib.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. #![doc = include_str!("../README.md")]
  2. #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
  3. #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
  4. use crate::focus::Focus;
  5. use anyhow::Result;
  6. use crossterm::{
  7. cursor::{MoveTo, RestorePosition, SavePosition, Show},
  8. event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
  9. execute,
  10. terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
  11. };
  12. use dioxus_native_core::{prelude::*, tree::TreeRef};
  13. use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, SendAnyMap};
  14. use focus::FocusState;
  15. use futures::{channel::mpsc::UnboundedSender, pin_mut, Future, StreamExt};
  16. use futures_channel::mpsc::unbounded;
  17. use layout::TaffyLayout;
  18. use prevent_default::PreventDefault;
  19. use ratatui::{backend::CrosstermBackend, Terminal};
  20. use std::{io, time::Duration};
  21. use std::{
  22. pin::Pin,
  23. sync::{Arc, Mutex},
  24. };
  25. use std::{rc::Rc, sync::RwLock};
  26. use style_attributes::StyleModifier;
  27. pub use taffy::{geometry::Point, prelude::*};
  28. use tokio::select;
  29. use widgets::{register_widgets, RinkWidgetResponder, RinkWidgetTraitObject};
  30. mod config;
  31. mod focus;
  32. mod hooks;
  33. mod layout;
  34. mod prevent_default;
  35. pub mod query;
  36. mod render;
  37. mod style;
  38. mod style_attributes;
  39. mod widget;
  40. mod widgets;
  41. pub use config::*;
  42. pub use hooks::*;
  43. pub use query::Query;
  44. // the layout space has a multiplier of 10 to minimize rounding errors
  45. pub(crate) fn screen_to_layout_space(screen: u16) -> f32 {
  46. screen as f32 * 10.0
  47. }
  48. pub(crate) fn unit_to_layout_space(screen: f32) -> f32 {
  49. screen * 10.0
  50. }
  51. pub(crate) fn layout_to_screen_space(layout: f32) -> f32 {
  52. layout / 10.0
  53. }
  54. #[derive(Clone)]
  55. pub struct TuiContext {
  56. tx: UnboundedSender<InputEvent>,
  57. }
  58. impl TuiContext {
  59. pub fn new(tx: UnboundedSender<InputEvent>) -> Self {
  60. Self { tx }
  61. }
  62. pub fn quit(&self) {
  63. // panic!("ack")
  64. self.tx.unbounded_send(InputEvent::Close).unwrap();
  65. }
  66. pub fn inject_event(&self, event: crossterm::event::Event) {
  67. self.tx
  68. .unbounded_send(InputEvent::UserInput(event))
  69. .unwrap();
  70. }
  71. }
  72. pub fn render<R: Driver>(
  73. cfg: Config,
  74. create_renderer: impl FnOnce(
  75. &Arc<RwLock<RealDom>>,
  76. &Arc<Mutex<Taffy>>,
  77. UnboundedSender<InputEvent>,
  78. ) -> R,
  79. ) -> Result<()> {
  80. let mut rdom = RealDom::new([
  81. TaffyLayout::to_type_erased(),
  82. Focus::to_type_erased(),
  83. StyleModifier::to_type_erased(),
  84. PreventDefault::to_type_erased(),
  85. ]);
  86. // Setup input handling
  87. // The event channel for fully resolved events
  88. let (event_tx, mut event_reciever) = unbounded();
  89. // The event channel for raw terminal events
  90. let (raw_event_tx, mut raw_event_reciever) = unbounded();
  91. let event_tx_clone = raw_event_tx.clone();
  92. if !cfg.headless {
  93. std::thread::spawn(move || {
  94. // Timeout after 10ms when waiting for events
  95. let tick_rate = Duration::from_millis(10);
  96. loop {
  97. if crossterm::event::poll(tick_rate).unwrap() {
  98. let evt = crossterm::event::read().unwrap();
  99. if raw_event_tx
  100. .unbounded_send(InputEvent::UserInput(evt))
  101. .is_err()
  102. {
  103. break;
  104. }
  105. }
  106. }
  107. });
  108. }
  109. register_widgets(&mut rdom, event_tx);
  110. let (handler, mut register_event) = RinkInputHandler::create(&mut rdom);
  111. let rdom = Arc::new(RwLock::new(rdom));
  112. let taffy = Arc::new(Mutex::new(Taffy::new()));
  113. let mut renderer = create_renderer(&rdom, &taffy, event_tx_clone);
  114. // insert the query engine into the rdom
  115. let query_engine = Query::new(rdom.clone(), taffy.clone());
  116. {
  117. let mut rdom = rdom.write().unwrap();
  118. rdom.raw_world_mut().add_unique(query_engine);
  119. }
  120. tokio::runtime::Builder::new_current_thread()
  121. .enable_all()
  122. .build()?
  123. .block_on(async {
  124. {
  125. renderer.update(&rdom);
  126. let mut any_map = SendAnyMap::new();
  127. any_map.insert(taffy.clone());
  128. let mut rdom = rdom.write().unwrap();
  129. let _ = rdom.update_state(any_map);
  130. }
  131. let mut terminal = (!cfg.headless).then(|| {
  132. enable_raw_mode().unwrap();
  133. let mut stdout = std::io::stdout();
  134. execute!(
  135. stdout,
  136. EnterAlternateScreen,
  137. EnableMouseCapture,
  138. MoveTo(0, 1000)
  139. )
  140. .unwrap();
  141. let backend = CrosstermBackend::new(io::stdout());
  142. Terminal::new(backend).unwrap()
  143. });
  144. if let Some(terminal) = &mut terminal {
  145. terminal.clear().unwrap();
  146. }
  147. let mut to_rerender = FxDashSet::default();
  148. to_rerender.insert(rdom.read().unwrap().root_id());
  149. let mut updated = true;
  150. loop {
  151. /*
  152. -> render the nodes in the right place with tui/crossterm
  153. -> wait for changes
  154. -> resolve events
  155. -> lazily update the layout and style based on nodes changed
  156. use simd to compare lines for diffing?
  157. todo: lazy re-rendering
  158. */
  159. if !to_rerender.is_empty() || updated {
  160. updated = false;
  161. fn resize(dims: ratatui::layout::Rect, taffy: &mut Taffy, rdom: &RealDom) {
  162. let width = screen_to_layout_space(dims.width);
  163. let height = screen_to_layout_space(dims.height);
  164. let root_node = rdom
  165. .get(rdom.root_id())
  166. .unwrap()
  167. .get::<TaffyLayout>()
  168. .unwrap()
  169. .node
  170. .unwrap();
  171. // the root node fills the entire area
  172. let mut style = taffy.style(root_node).unwrap().clone();
  173. let new_size = Size {
  174. width: Dimension::Points(width),
  175. height: Dimension::Points(height),
  176. };
  177. if style.size != new_size {
  178. style.size = new_size;
  179. taffy.set_style(root_node, style).unwrap();
  180. }
  181. let size = Size {
  182. width: AvailableSpace::Definite(width),
  183. height: AvailableSpace::Definite(height),
  184. };
  185. taffy.compute_layout(root_node, size).unwrap();
  186. }
  187. if let Some(terminal) = &mut terminal {
  188. execute!(terminal.backend_mut(), SavePosition).unwrap();
  189. terminal.draw(|frame| {
  190. let rdom = rdom.write().unwrap();
  191. let mut taffy = taffy.lock().expect("taffy lock poisoned");
  192. // size is guaranteed to not change when rendering
  193. resize(frame.size(), &mut taffy, &rdom);
  194. let root = rdom.get(rdom.root_id()).unwrap();
  195. render::render_vnode(frame, &taffy, root, cfg, Point::ZERO);
  196. })?;
  197. execute!(terminal.backend_mut(), RestorePosition, Show).unwrap();
  198. } else {
  199. let rdom = rdom.read().unwrap();
  200. resize(
  201. ratatui::layout::Rect {
  202. x: 0,
  203. y: 0,
  204. width: 1000,
  205. height: 1000,
  206. },
  207. &mut taffy.lock().expect("taffy lock poisoned"),
  208. &rdom,
  209. );
  210. }
  211. }
  212. let mut event_recieved = None;
  213. {
  214. let wait = renderer.poll_async();
  215. pin_mut!(wait);
  216. select! {
  217. _ = wait => {
  218. },
  219. evt = raw_event_reciever.next() => {
  220. match evt.as_ref().unwrap() {
  221. InputEvent::UserInput(event) => match event {
  222. TermEvent::Key(key) => {
  223. if matches!(key.code, KeyCode::Char('C' | 'c'))
  224. && key.modifiers.contains(KeyModifiers::CONTROL)
  225. && cfg.ctrl_c_quit
  226. {
  227. break;
  228. }
  229. }
  230. TermEvent::Resize(_, _) => updated = true,
  231. _ => {}
  232. },
  233. InputEvent::Close => break,
  234. };
  235. if let InputEvent::UserInput(evt) = evt.unwrap() {
  236. register_event(evt);
  237. }
  238. },
  239. Some(evt) = event_reciever.next() => {
  240. event_recieved = Some(evt);
  241. }
  242. }
  243. }
  244. {
  245. if let Some(evt) = event_recieved {
  246. renderer.handle_event(
  247. &rdom,
  248. evt.id,
  249. evt.name,
  250. Rc::new(evt.data),
  251. evt.bubbles,
  252. );
  253. }
  254. {
  255. let evts = handler.get_events(
  256. &taffy.lock().expect("taffy lock poisoned"),
  257. &mut rdom.write().unwrap(),
  258. );
  259. updated |= handler.state().focus_state.clean();
  260. for e in evts {
  261. bubble_event_to_widgets(&mut rdom.write().unwrap(), &e);
  262. renderer.handle_event(&rdom, e.id, e.name, Rc::new(e.data), e.bubbles);
  263. }
  264. }
  265. // updates the dom's nodes
  266. renderer.update(&rdom);
  267. // update the style and layout
  268. let mut rdom = rdom.write().unwrap();
  269. let mut any_map = SendAnyMap::new();
  270. any_map.insert(taffy.clone());
  271. let (new_to_rerender, dirty) = rdom.update_state(any_map);
  272. to_rerender = new_to_rerender;
  273. let text_mask = NodeMaskBuilder::new().with_text().build();
  274. for (id, mask) in dirty {
  275. if mask.overlaps(&text_mask) {
  276. to_rerender.insert(id);
  277. }
  278. }
  279. }
  280. }
  281. if let Some(terminal) = &mut terminal {
  282. disable_raw_mode()?;
  283. execute!(
  284. terminal.backend_mut(),
  285. LeaveAlternateScreen,
  286. DisableMouseCapture
  287. )?;
  288. terminal.show_cursor()?;
  289. }
  290. Ok(())
  291. })
  292. }
  293. #[derive(Debug)]
  294. pub enum InputEvent {
  295. UserInput(TermEvent),
  296. Close,
  297. }
  298. pub trait Driver {
  299. fn update(&mut self, rdom: &Arc<RwLock<RealDom>>);
  300. fn handle_event(
  301. &mut self,
  302. rdom: &Arc<RwLock<RealDom>>,
  303. id: NodeId,
  304. event: &str,
  305. value: Rc<EventData>,
  306. bubbles: bool,
  307. );
  308. fn poll_async(&mut self) -> Pin<Box<dyn Future<Output = ()> + '_>>;
  309. }
  310. /// Before sending the event to drivers, we need to bubble it up the tree to any widgets that are listening
  311. fn bubble_event_to_widgets(rdom: &mut RealDom, event: &Event) {
  312. let id = event.id;
  313. let mut node = Some(id);
  314. while let Some(node_id) = node {
  315. let parent_id = {
  316. let tree = rdom.tree_ref();
  317. tree.parent_id_advanced(node_id, true)
  318. };
  319. {
  320. // println!("@ bubbling event to node {:?}", node_id);
  321. let mut node_mut = rdom.get_mut(node_id).unwrap();
  322. if let Some(mut widget) = node_mut
  323. .get_mut::<RinkWidgetTraitObject>()
  324. .map(|w| w.clone())
  325. {
  326. widget.handle_event(event, node_mut)
  327. }
  328. }
  329. if !event.bubbles {
  330. // println!("event does not bubble");
  331. break;
  332. }
  333. node = parent_id;
  334. }
  335. }
  336. pub(crate) fn get_abs_layout(node: NodeRef, taffy: &Taffy) -> Layout {
  337. let mut node_layout = *taffy
  338. .layout(node.get::<TaffyLayout>().unwrap().node.unwrap())
  339. .unwrap();
  340. let mut current = node;
  341. let dom = node.real_dom();
  342. let tree = dom.tree_ref();
  343. while let Some(parent) = tree.parent_id_advanced(current.id(), true) {
  344. let parent = dom.get(parent).unwrap();
  345. current = parent;
  346. let parent_layout = taffy
  347. .layout(parent.get::<TaffyLayout>().unwrap().node.unwrap())
  348. .unwrap();
  349. node_layout.location.x += parent_layout.location.x;
  350. node_layout.location.y += parent_layout.location.y;
  351. }
  352. node_layout
  353. }