1use std::io;
2use std::time::Duration;
3
4use ansiq_core::Rect;
5use ansiq_render::{
6 FrameBuffer, diff_buffers, diff_buffers_in_regions, frame_patches, render_cursor_at_origin,
7 render_patches_at_origin,
8};
9use ansiq_surface::{InputEvent, InputEventStream, TerminalSession, Viewport, ViewportPolicy};
10
11use crate::{App, Engine, cursor_position, draw_tree, draw_tree_in_regions};
12
13pub fn viewport_bounds(viewport: Viewport) -> Rect {
14 Rect::new(0, 0, viewport.width, viewport.height)
15}
16
17pub fn exit_row_for_content(viewport: Viewport, required_height: u16) -> u16 {
18 let used_height = required_height.clamp(1, viewport.height);
19 viewport
20 .origin_y
21 .saturating_add(used_height.saturating_sub(1))
22}
23
24pub async fn run_app<A: App>(app: A) -> io::Result<()> {
25 run_app_with_policy(app, ViewportPolicy::PreserveVisible).await
26}
27
28pub async fn run_app_with_policy<A: App>(app: A, policy: ViewportPolicy) -> io::Result<()> {
29 let mut terminal = TerminalSession::enter(policy)?;
30 let mut viewport = terminal.viewport();
31 let mut input_stream = InputEventStream::default();
32
33 let mut engine = Engine::new(app);
34 engine.mount();
35 engine.set_bounds(viewport_bounds(viewport));
36 engine.render_tree();
37 if flush_committed_history(&mut terminal, &mut viewport, policy, &mut engine)? {
38 engine.render_tree();
39 }
40 sync_viewport_to_content(&mut terminal, &mut viewport, policy, &mut engine)?;
41
42 let mut previous = FrameBuffer::new(viewport.width, viewport.height);
43 render_current_frame(&mut terminal, &engine, viewport.origin_y, &mut previous)?;
44
45 loop {
46 if engine.drain_requests() {
47 break;
48 }
49
50 if let Some(event) = input_stream.next_event(Duration::from_millis(16)).await? {
51 match event {
52 InputEvent::Resize(width, height) => {
53 viewport = terminal.resize(policy, (width, height));
54 engine.set_bounds(viewport_bounds(viewport));
55 previous = FrameBuffer::new(viewport.width, viewport.height);
56 }
57 InputEvent::Key(key) => {
58 if engine.handle_input(key) {
59 break;
60 }
61 }
62 }
63 }
64
65 if engine.drain_requests() {
66 break;
67 }
68
69 if engine.is_dirty() {
70 engine.render_tree();
71 if flush_committed_history(&mut terminal, &mut viewport, policy, &mut engine)? {
72 previous = FrameBuffer::new(viewport.width, viewport.height);
73 engine.render_tree();
74 }
75 if sync_viewport_to_content(&mut terminal, &mut viewport, policy, &mut engine)? {
76 previous = FrameBuffer::new(viewport.width, viewport.height);
77 }
78 render_current_frame(&mut terminal, &engine, viewport.origin_y, &mut previous)?;
79 }
80
81 tokio::task::yield_now().await;
82 }
83
84 Ok(())
85}
86
87fn flush_committed_history<A: App>(
88 terminal: &mut TerminalSession,
89 viewport: &mut Viewport,
90 policy: ViewportPolicy,
91 engine: &mut Engine<A>,
92) -> io::Result<bool> {
93 let history = engine.take_pending_history();
94 if history.is_empty() {
95 return Ok(false);
96 }
97
98 *viewport = terminal.commit_history_blocks(history, policy)?;
100 engine.set_bounds(viewport_bounds(*viewport));
101 Ok(true)
102}
103
104fn sync_viewport_to_content<A: App>(
105 terminal: &mut TerminalSession,
106 viewport: &mut Viewport,
107 policy: ViewportPolicy,
108 engine: &mut Engine<A>,
109) -> io::Result<bool> {
110 let Some(requested_height) = policy.requested_height(viewport.height, engine.required_height())
111 else {
112 return Ok(false);
113 };
114
115 terminal.reserve_inline_space(requested_height)?;
118 *viewport = terminal.viewport();
119 engine.set_bounds(viewport_bounds(*viewport));
120 engine.render_tree();
121 Ok(true)
122}
123
124fn render_current_frame<A: App>(
125 terminal: &mut TerminalSession,
126 engine: &Engine<A>,
127 origin_y: u16,
128 previous: &mut FrameBuffer,
129) -> io::Result<()> {
130 let Some(tree) = engine.tree() else {
131 return Ok(());
132 };
133
134 let next = if let Some(regions) = engine.redraw_regions() {
135 let mut next = previous.clone();
136 draw_tree_in_regions(tree, engine.focused(), &mut next, regions);
137 next
138 } else {
139 let mut next = FrameBuffer::new(previous.width(), previous.height());
140 draw_tree(tree, engine.focused(), &mut next);
141 next
142 };
143 let cursor = cursor_position(tree, engine.focused());
144
145 let patches = if let Some(regions) = engine.redraw_regions() {
146 diff_buffers_in_regions(previous, &next, regions)
147 } else if previous.is_blank() {
148 frame_patches(&next)
149 } else {
150 diff_buffers(previous, &next)
151 };
152 let mut bytes = Vec::new();
153 if !patches.is_empty() {
154 render_patches_at_origin(&mut bytes, &patches, origin_y)?;
155 }
156 render_cursor_at_origin(&mut bytes, cursor, origin_y)?;
157 let output = String::from_utf8(bytes).expect("rendered patches should be valid utf-8");
158 terminal.write_ansi(&output)?;
159 terminal.set_exit_row(exit_row_for_content(
160 terminal.viewport(),
161 engine.required_height(),
162 ));
163
164 *previous = next;
165 Ok(())
166}