Skip to main content

gallery/
main.rs

1//! Interactive rendering gallery: every showcase scene as one tabbed app.
2//!
3//! ```sh
4//! cargo run -p omp-tui --example gallery
5//! ```
6//!
7//! Tab/Shift-Tab moves focus, ←/→ switches tabs on the tab bar, ↑/↓ and
8//! PageUp/PageDown scroll the active pane, and the Live tab re-renders the
9//! preview as you type. The `Anim` tab autoplays its prop tweens and takes
10//! scene keys, the `Overlay` tab opens modal layers (`Ctrl+K`/`Ctrl+G`),
11//! `Eclipse` runs the fullscreen shader, and `Picker` hosts the chat
12//! example's model switcher inline. Ctrl-C or Ctrl-Q quits.
13
14mod anim;
15mod eclipse;
16mod overlay;
17mod render;
18
19#[allow(
20	dead_code,
21	reason = "the shared picker module also carries the chat example's overlay driver"
22)]
23#[path = "../chat/picker.rs"]
24mod picker;
25
26use std::io;
27
28use omp_tui::{AppEvent, AppOptions, Key, OverlayId, Size, Ui, UiContext, dom};
29
30/// List rows granted to the inline picker tab.
31const PICKER_ROWS: u16 = 8;
32
33fn build_ui(viewport: Size, context: UiContext) -> Ui {
34	// keep panes inside the viewport so switching tabs never strands
35	// stale rows in scrollback
36	let pane_height = render::pane_height(viewport);
37	let charset = context.charset;
38
39	Ui::from_root(
40		dom! {
41			<col gap=1>
42				<tabs id=view>
43					<tab title="Markdown">
44						<scroll id="pane-md" h={pane_height}>
45							<md>{render::MARKDOWN_TAB}</md>
46						</scroll>
47					</tab>
48					<tab title="Math">
49						<scroll id="pane-math" h={pane_height}>
50							<md>{render::MATH_TAB}</md>
51						</scroll>
52					</tab>
53					<tab title="Mermaid">
54						<scroll id="pane-mermaid" h={pane_height}>
55							<md>{render::MERMAID_TAB}</md>
56						</scroll>
57					</tab>
58					<tab title="Graphviz">
59						<scroll id="pane-graphviz" h={pane_height}>
60							<md>{render::GRAPHVIZ_TAB}</md>
61						</scroll>
62					</tab>
63					<tab title="Macro">
64						<box border=round title="Built with dom!">
65							<col gap=1>
66								<row gap=1>
67									<i:info/>
68									<text bold>{"Macro-built pane"}</text>
69								</row>
70								<gallery-note>
71									<text dim>{format!("Interpolated at runtime: {} nested layout levels", 3)}</text>
72								</gallery-note>
73							</col>
74						</box>
75					</tab>
76					<tab title="Live">
77						<col gap=1>
78							<editor id=src value={render::LIVE_PREFILL}/>
79							<box border=round title="Preview">
80								<md id=preview>{"..."}</md>
81							</box>
82						</col>
83					</tab>
84					<tab title="Anim">{anim::pane()}</tab>
85					<tab title="Overlay">{overlay::pane()}</tab>
86					<tab title="Eclipse">{eclipse::pane(viewport, pane_height)}</tab>
87					<tab title="Picker">
88						{picker::models_pane(0, PICKER_ROWS, viewport.width, charset)}
89					</tab>
90				</tabs>
91				<text dim>{"Tab focus · ←/→ switch tabs · ↑/↓ PgUp/PgDn scroll · Ctrl-C quit"}</text>
92			</col>
93		},
94		viewport.width,
95		context,
96	)
97}
98
99/// Title of the active tab, from the tabs component's reported value.
100fn active_tab(ui: &Ui) -> String {
101	ui.values()["view"].as_str().unwrap_or_default().to_owned()
102}
103
104/// Layers opened from the Overlay tab.
105#[derive(Default)]
106struct Layers {
107	picker: Option<OverlayId>,
108	help:   Option<OverlayId>,
109}
110
111#[tokio::main]
112async fn main() -> io::Result<()> {
113	let mut app = AppOptions::new()
114		.mouse()
115		.quit([Key::Ctrl('c'), Key::Ctrl('q')])
116		.start(|env| build_ui(env.viewport, env.ctx))
117		.await?;
118	// The picker tab opens with the first model's details, like the chat
119	// overlay does.
120	picker::show_detail_on(app.ui_mut(), Some(0));
121
122	let mut synced = String::new();
123	let mut lab = anim::Lab::new();
124	let mut layers = Layers::default();
125	let mut next_step = tokio::time::Instant::now() + anim::AUTOPLAY_STEP;
126
127	loop {
128		let event = tokio::select! {
129			event = app.next() => match event? {
130				Some(event) => event,
131				None => break,
132			},
133			() = tokio::time::sleep_until(next_step) => {
134				if lab.autoplay && active_tab(app.ui()) == "Anim" {
135					lab.advance(app.ui_mut());
136				}
137				next_step += anim::AUTOPLAY_STEP;
138				continue;
139			},
140		};
141		match event {
142			AppEvent::Resized(viewport) => {
143				for pane in render::PANE_IDS {
144					app.ui_mut().set_height(pane, render::pane_height(viewport));
145				}
146			},
147			AppEvent::Key(key) => match active_tab(app.ui()).as_str() {
148				"Anim" => lab.handle_key(key, app.ui_mut()),
149				"Overlay" => match key {
150					Key::Ctrl('k') if layers.picker.is_none() => {
151						layers.picker = Some(overlay::show_picker(app.ui_mut()));
152					},
153					Key::Ctrl('g') => match layers.help.take() {
154						Some(id) => {
155							app.ui_mut().close_overlay(id);
156						},
157						None => layers.help = Some(overlay::show_help(app.ui_mut())),
158					},
159					_ => {},
160				},
161				_ => {},
162			},
163			// The Overlay tab's modal select committed a model.
164			AppEvent::Changed { id, value } if id == "model" => {
165				if let Some(overlay) = layers.picker.take() {
166					let label = overlay::MODELS
167						.iter()
168						.find(|(short, ..)| *short == value)
169						.map_or(value.as_str(), |(_, label, _)| label);
170					app.ui_mut().set_text("status", format!("model: {label}"));
171					app.ui_mut().close_overlay(overlay);
172				}
173			},
174			// The Picker tab's select moved: mirror the chat picker's
175			// facts-and-chips detail line.
176			AppEvent::Highlighted { id, value } if id == "models" => {
177				picker::show_detail_on(app.ui_mut(), value.as_str().parse().ok());
178			},
179			AppEvent::Filtered { id, value, .. } if id == "models" => {
180				let model = value.and_then(|value| value.as_str().parse().ok());
181				picker::show_detail_on(app.ui_mut(), model);
182			},
183			AppEvent::OverlayClosed(id) => {
184				if layers.picker == Some(id) {
185					layers.picker = None;
186				}
187				if layers.help == Some(id) {
188					layers.help = None;
189				}
190			},
191			_ => {},
192		}
193		render::sync_preview(app.ui_mut(), &mut synced);
194		// Reserve the Overlay tab's chords only while it is showing, so the
195		// focused composer can't spend Ctrl+K on kill-line — and the Live
196		// tab's editor keeps it.
197		let chords: &[Key] = if active_tab(app.ui()) == "Overlay" {
198			&[Key::Ctrl('k'), Key::Ctrl('g')]
199		} else {
200			&[]
201		};
202		app.set_hotkeys(chords.iter().copied());
203	}
204	Ok(())
205}