Skip to main content

max / quasi

7.1 KB · 179 lines History Blame Raw
1 //! Regions into panels.
2 //!
3 //! The half of the drawing a webview never has to do. A stylesheet turns
4 //! `list-detail` into two columns and the browser does the arithmetic; here it is
5 //! the renderer's, and every place the description does not say enough to do it
6 //! is a finding.
7 //!
8 //! The three findings `quasi-tui`'s `region` module records were about the
9 //! description rather than about terminals, so they applied here too, and all
10 //! three are answered: [`Arrangement::share`] carries a region's share,
11 //! [`layout::Showing`] says which child a region is showing, and `Slot::label`
12 //! names one. Reading the last two is `node::showing_body`, which this renderer
13 //! got last of the three and until then drew a described tab group as every
14 //! panel stacked.
15 //!
16 //! What differs from the terminal is only the mechanism: `egui::SidePanel` and
17 //! `Ui::columns` rather than rect arithmetic, and a `ScrollArea` per pane
18 //! because scroll is egui's here and the terminal's there.
19
20 use egui::Ui;
21 use quasi_router::layout::Arrangement;
22 use quasi_router::{RegionKind, Screen, Slot};
23
24 use crate::{Pass, node};
25
26 /// Lay a screen's regions out and draw them.
27 ///
28 /// Bands take the height they need and get out of the way; which way they get
29 /// out of is where they were said.
30 ///
31 /// What the ruling settled was a disagreement, not a silence. quasi-webview
32 /// emits `screen.slots` in declaration order and always has; this renderer and
33 /// quasi-tui both hoisted every band to the top, so the same description drew
34 /// two different screens depending on who was drawing it. Order was already
35 /// load-bearing for the one renderer with live screens.
36 pub(crate) fn screen_regions(pass: &mut Pass<'_>, ui: &mut Ui, screen: &Screen) {
37 let (leading, body, trailing) = split(screen);
38
39 for slot in &leading {
40 node::region(pass, ui, slot);
41 }
42
43 // The trailing bands first, from the bottom up, so the body is arranged in
44 // what is left rather than in all of it. egui gives no way to ask a region
45 // how tall it will be without drawing it, and a reserved guess would be the
46 // authored number the whole layer exists to refuse; a bottom-up layout is
47 // the host's own arithmetic answering the same question exactly.
48 //
49 // Reversed, because the first widget in a bottom-up layout is the lowest
50 // one: the band said last is the band at the bottom.
51 if trailing.is_empty() {
52 arranged(pass, ui, screen, &body);
53 } else {
54 ui.with_layout(egui::Layout::bottom_up(egui::Align::Min), |ui| {
55 for slot in trailing.iter().rev() {
56 node::region(pass, ui, slot);
57 }
58 ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {
59 arranged(pass, ui, screen, &body);
60 });
61 });
62 }
63 }
64
65 /// A screen's slots as the bands above the body, the body, and the bands below.
66 ///
67 /// The split is at the first region that is not a band. A screen of nothing but
68 /// bands is all leading, which keeps the shape every description written before
69 /// the ruling already had.
70 fn split(screen: &Screen) -> (Vec<&Slot>, Vec<&Slot>, Vec<&Slot>) {
71 let first_body = screen
72 .slots
73 .iter()
74 .position(|slot| !matches!(slot.kind, RegionKind::Band));
75 let Some(at) = first_body else {
76 return (screen.slots.iter().collect(), Vec::new(), Vec::new());
77 };
78 let is_band = |slot: &&Slot| matches!(slot.kind, RegionKind::Band);
79 (
80 screen.slots[..at].iter().collect(),
81 screen.slots[at..]
82 .iter()
83 .filter(|slot| !is_band(slot))
84 .collect(),
85 screen.slots[at..].iter().filter(is_band).collect(),
86 )
87 }
88
89 /// The body regions, laid out the way the screen said.
90 fn arranged(pass: &mut Pass<'_>, ui: &mut Ui, screen: &Screen, body: &[&Slot]) {
91 match screen.arrangement {
92 Arrangement::SidebarContent { share } => {
93 let sidebar = body
94 .iter()
95 .position(|slot| matches!(slot.kind, RegionKind::Sidebar));
96 match sidebar {
97 Some(at) => {
98 let width = ui.available_width() * share_of(share);
99 ui.horizontal_top(|ui| {
100 ui.allocate_ui(egui::vec2(width, ui.available_height()), |ui| {
101 scrolled(ui, body[at], pass);
102 });
103 ui.vertical(|ui| {
104 for (i, slot) in body.iter().enumerate() {
105 if i != at {
106 scrolled(ui, slot, pass);
107 }
108 }
109 });
110 });
111 }
112 // A sidebar arrangement with no sidebar region is a description
113 // that named a shape it did not fill. Drawn down the page
114 // rather than refused: the regions are real and the reader can
115 // still use them.
116 None => {
117 for slot in body {
118 scrolled(ui, slot, pass);
119 }
120 }
121 }
122 }
123
124 Arrangement::ListDetail { share, tabbed } => {
125 if tabbed {
126 // The guess named in the header: nothing says which of the two
127 // is showing, so the first is.
128 if let Some(slot) = body.first() {
129 scrolled(ui, slot, pass);
130 }
131 } else {
132 let left = ui.available_width() * share_of(share);
133 ui.horizontal_top(|ui| {
134 if let Some(slot) = body.first() {
135 ui.allocate_ui(egui::vec2(left, ui.available_height()), |ui| {
136 scrolled(ui, slot, pass);
137 });
138 }
139 ui.vertical(|ui| {
140 for slot in body.iter().skip(1) {
141 scrolled(ui, slot, pass);
142 }
143 });
144 });
145 }
146 }
147
148 // One region filling the frame. Down the page, because there is no
149 // division to put anything beside.
150 Arrangement::Single => {
151 for slot in body {
152 scrolled(ui, slot, pass);
153 }
154 }
155 }
156 }
157
158 /// A pane with its own scroll.
159 ///
160 /// Scroll is egui's here, which is one of the three things `quasi-tui`'s `View`
161 /// holds and this crate's does not. The id is the slot's, so a pane keeps its
162 /// position across frames the way the description's stable ids intend.
163 fn scrolled(ui: &mut Ui, slot: &Slot, pass: &mut Pass<'_>) {
164 egui::ScrollArea::vertical()
165 .id_salt(&slot.id)
166 .show(ui, |ui| {
167 node::region(pass, ui, slot);
168 });
169 }
170
171 /// A share as a fraction of the width on offer.
172 fn share_of(share: quasi_router::layout::Share) -> f32 {
173 // `Share::of` answers in whole columns for a terminal. A window has
174 // fractional width, so the ratio is taken against a large denominator and
175 // divided back, which keeps one fact behind both renderers' proportions.
176 const SCALE: u16 = 1000;
177 f32::from(share.of(SCALE)) / f32::from(SCALE)
178 }
179