lib.rs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. use anyhow::Result;
  2. use crossterm::{
  3. event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
  4. execute,
  5. terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
  6. };
  7. use dioxus_core::exports::futures_channel::mpsc::unbounded;
  8. use dioxus_core::*;
  9. use futures::{
  10. channel::mpsc::{UnboundedReceiver, UnboundedSender},
  11. pin_mut, StreamExt,
  12. };
  13. use std::{
  14. collections::HashMap,
  15. io,
  16. time::{Duration, Instant},
  17. };
  18. use stretch2::{
  19. prelude::{Node, Size},
  20. Stretch,
  21. };
  22. use style::RinkStyle;
  23. use tui::{backend::CrosstermBackend, Terminal};
  24. mod attributes;
  25. mod config;
  26. mod hooks;
  27. mod layout;
  28. mod render;
  29. mod style;
  30. mod widget;
  31. pub use attributes::*;
  32. pub use config::*;
  33. pub use hooks::*;
  34. pub use layout::*;
  35. pub use render::*;
  36. #[derive(Clone)]
  37. pub struct TuiContext {
  38. tx: UnboundedSender<InputEvent>,
  39. }
  40. impl TuiContext {
  41. pub fn quit(&self) {
  42. self.tx.unbounded_send(InputEvent::Close).unwrap();
  43. }
  44. }
  45. pub fn launch(app: Component<()>) {
  46. launch_cfg(app, Config::default())
  47. }
  48. pub fn launch_cfg(app: Component<()>, cfg: Config) {
  49. let mut dom = VirtualDom::new(app);
  50. let (tx, rx) = unbounded();
  51. // Setup input handling
  52. let (event_tx, event_rx) = unbounded();
  53. let event_tx_clone = event_tx.clone();
  54. if !cfg.headless {
  55. std::thread::spawn(move || {
  56. let tick_rate = Duration::from_millis(100);
  57. let mut last_tick = Instant::now();
  58. loop {
  59. // poll for tick rate duration, if no events, sent tick event.
  60. let timeout = tick_rate
  61. .checked_sub(last_tick.elapsed())
  62. .unwrap_or_else(|| Duration::from_secs(0));
  63. if crossterm::event::poll(timeout).unwrap() {
  64. let evt = crossterm::event::read().unwrap();
  65. event_tx.unbounded_send(InputEvent::UserInput(evt)).unwrap();
  66. }
  67. if last_tick.elapsed() >= tick_rate {
  68. event_tx.unbounded_send(InputEvent::Tick).unwrap();
  69. last_tick = Instant::now();
  70. }
  71. }
  72. });
  73. }
  74. let cx = dom.base_scope();
  75. cx.provide_root_context(TuiContext { tx: event_tx_clone });
  76. let (handler, state) = RinkInputHandler::new(rx, cx);
  77. cx.provide_root_context(state);
  78. dom.rebuild();
  79. render_vdom(&mut dom, event_rx, tx, handler, cfg).unwrap();
  80. }
  81. pub struct TuiNode<'a> {
  82. pub layout: stretch2::node::Node,
  83. pub block_style: RinkStyle,
  84. pub tui_modifier: TuiModifier,
  85. pub node: &'a VNode<'a>,
  86. }
  87. fn render_vdom(
  88. vdom: &mut VirtualDom,
  89. mut event_reciever: UnboundedReceiver<InputEvent>,
  90. ctx: UnboundedSender<TermEvent>,
  91. handler: RinkInputHandler,
  92. cfg: Config,
  93. ) -> Result<()> {
  94. tokio::runtime::Builder::new_current_thread()
  95. .enable_all()
  96. .build()?
  97. .block_on(async {
  98. /*
  99. Get the terminal to calcualte the layout from
  100. */
  101. let mut terminal = (!cfg.headless).then(|| {
  102. enable_raw_mode().unwrap();
  103. let mut stdout = std::io::stdout();
  104. execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
  105. let backend = CrosstermBackend::new(io::stdout());
  106. Terminal::new(backend).unwrap()
  107. });
  108. if let Some(terminal) = &mut terminal {
  109. terminal.clear().unwrap();
  110. }
  111. loop {
  112. /*
  113. -> collect all the nodes with their layout
  114. -> solve their layout
  115. -> resolve events
  116. -> render the nodes in the right place with tui/crosstream
  117. -> while rendering, apply styling
  118. use simd to compare lines for diffing?
  119. todo: reuse the layout and node objects.
  120. our work_with_deadline method can tell us which nodes are dirty.
  121. */
  122. let mut layout = Stretch::new();
  123. let mut nodes = HashMap::new();
  124. let root_node = vdom.base_scope().root_node();
  125. layout::collect_layout(&mut layout, &mut nodes, vdom, root_node);
  126. /*
  127. Compute the layout given the terminal size
  128. */
  129. let node_id = root_node.try_mounted_id().unwrap();
  130. let root_layout = nodes[&node_id].layout;
  131. let mut events = Vec::new();
  132. fn resize(dims: tui::layout::Rect, stretch: &mut Stretch, root_layout: Node) {
  133. let width = dims.width;
  134. let height = dims.height;
  135. stretch
  136. .compute_layout(
  137. root_layout,
  138. Size {
  139. width: stretch2::prelude::Number::Defined((width - 1) as f32),
  140. height: stretch2::prelude::Number::Defined((height - 1) as f32),
  141. },
  142. )
  143. .unwrap();
  144. }
  145. if let Some(terminal) = &mut terminal {
  146. terminal.draw(|frame| {
  147. // size is guaranteed to not change when rendering
  148. resize(frame.size(), &mut layout, root_layout);
  149. // resolve events before rendering
  150. events = handler.get_events(vdom, &layout, &mut nodes, root_node);
  151. render::render_vnode(
  152. frame,
  153. &layout,
  154. &mut nodes,
  155. vdom,
  156. root_node,
  157. &RinkStyle::default(),
  158. cfg,
  159. );
  160. assert!(nodes.is_empty());
  161. })?;
  162. } else {
  163. resize(
  164. tui::layout::Rect {
  165. x: 0,
  166. y: 0,
  167. width: 100,
  168. height: 100,
  169. },
  170. &mut layout,
  171. root_layout,
  172. );
  173. }
  174. for e in events {
  175. vdom.handle_message(SchedulerMsg::Event(e));
  176. }
  177. use futures::future::{select, Either};
  178. {
  179. let wait = vdom.wait_for_work();
  180. pin_mut!(wait);
  181. match select(wait, event_reciever.next()).await {
  182. Either::Left((_a, _b)) => {
  183. //
  184. }
  185. Either::Right((evt, _o)) => {
  186. match evt.as_ref().unwrap() {
  187. InputEvent::UserInput(event) => match event {
  188. TermEvent::Key(key) => {
  189. if matches!(key.code, KeyCode::Char('C' | 'c'))
  190. && key.modifiers.contains(KeyModifiers::CONTROL)
  191. && cfg.ctrl_c_quit
  192. {
  193. break;
  194. }
  195. }
  196. TermEvent::Resize(_, _) | TermEvent::Mouse(_) => {}
  197. },
  198. InputEvent::Tick => {} // tick
  199. InputEvent::Close => break,
  200. };
  201. if let InputEvent::UserInput(evt) = evt.unwrap() {
  202. ctx.unbounded_send(evt).unwrap();
  203. }
  204. }
  205. }
  206. }
  207. vdom.work_with_deadline(|| false);
  208. }
  209. if let Some(terminal) = &mut terminal {
  210. disable_raw_mode()?;
  211. execute!(
  212. terminal.backend_mut(),
  213. LeaveAlternateScreen,
  214. DisableMouseCapture
  215. )?;
  216. terminal.show_cursor()?;
  217. }
  218. Ok(())
  219. })
  220. }
  221. enum InputEvent {
  222. UserInput(TermEvent),
  223. Tick,
  224. #[allow(dead_code)]
  225. Close,
  226. }