//! Regions into panels. //! //! The half of the drawing a webview never has to do. A stylesheet turns //! `list-detail` into two columns and the browser does the arithmetic; here it is //! the renderer's, and every place the description does not say enough to do it //! is a finding. //! //! The three findings `quasi-tui`'s `region` module records were about the //! description rather than about terminals, so they applied here too, and all //! three are answered: [`Arrangement::share`] carries a region's share, //! [`layout::Showing`] says which child a region is showing, and `Slot::label` //! names one. Reading the last two is `node::showing_body`, which this renderer //! got last of the three and until then drew a described tab group as every //! panel stacked. //! //! What differs from the terminal is only the mechanism: `egui::SidePanel` and //! `Ui::columns` rather than rect arithmetic, and a `ScrollArea` per pane //! because scroll is egui's here and the terminal's there. use egui::Ui; use quasi_router::layout::Arrangement; use quasi_router::{RegionKind, Screen, Slot}; use crate::{Pass, node}; /// Lay a screen's regions out and draw them. /// /// Bands take the height they need and get out of the way; which way they get /// out of is where they were said. /// /// What the ruling settled was a disagreement, not a silence. quasi-webview /// emits `screen.slots` in declaration order and always has; this renderer and /// quasi-tui both hoisted every band to the top, so the same description drew /// two different screens depending on who was drawing it. Order was already /// load-bearing for the one renderer with live screens. pub(crate) fn screen_regions(pass: &mut Pass<'_>, ui: &mut Ui, screen: &Screen) { let (leading, body, trailing) = split(screen); for slot in &leading { node::region(pass, ui, slot); } // The trailing bands first, from the bottom up, so the body is arranged in // what is left rather than in all of it. egui gives no way to ask a region // how tall it will be without drawing it, and a reserved guess would be the // authored number the whole layer exists to refuse; a bottom-up layout is // the host's own arithmetic answering the same question exactly. // // Reversed, because the first widget in a bottom-up layout is the lowest // one: the band said last is the band at the bottom. if trailing.is_empty() { arranged(pass, ui, screen, &body); } else { ui.with_layout(egui::Layout::bottom_up(egui::Align::Min), |ui| { for slot in trailing.iter().rev() { node::region(pass, ui, slot); } ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| { arranged(pass, ui, screen, &body); }); }); } } /// A screen's slots as the bands above the body, the body, and the bands below. /// /// The split is at the first region that is not a band. A screen of nothing but /// bands is all leading, which keeps the shape every description written before /// the ruling already had. fn split(screen: &Screen) -> (Vec<&Slot>, Vec<&Slot>, Vec<&Slot>) { let first_body = screen .slots .iter() .position(|slot| !matches!(slot.kind, RegionKind::Band)); let Some(at) = first_body else { return (screen.slots.iter().collect(), Vec::new(), Vec::new()); }; let is_band = |slot: &&Slot| matches!(slot.kind, RegionKind::Band); ( screen.slots[..at].iter().collect(), screen.slots[at..] .iter() .filter(|slot| !is_band(slot)) .collect(), screen.slots[at..].iter().filter(is_band).collect(), ) } /// The body regions, laid out the way the screen said. fn arranged(pass: &mut Pass<'_>, ui: &mut Ui, screen: &Screen, body: &[&Slot]) { match screen.arrangement { Arrangement::SidebarContent { share } => { let sidebar = body .iter() .position(|slot| matches!(slot.kind, RegionKind::Sidebar)); match sidebar { Some(at) => { let width = ui.available_width() * share_of(share); ui.horizontal_top(|ui| { ui.allocate_ui(egui::vec2(width, ui.available_height()), |ui| { scrolled(ui, body[at], pass); }); ui.vertical(|ui| { for (i, slot) in body.iter().enumerate() { if i != at { scrolled(ui, slot, pass); } } }); }); } // A sidebar arrangement with no sidebar region is a description // that named a shape it did not fill. Drawn down the page // rather than refused: the regions are real and the reader can // still use them. None => { for slot in body { scrolled(ui, slot, pass); } } } } Arrangement::ListDetail { share, tabbed } => { if tabbed { // The guess named in the header: nothing says which of the two // is showing, so the first is. if let Some(slot) = body.first() { scrolled(ui, slot, pass); } } else { let left = ui.available_width() * share_of(share); ui.horizontal_top(|ui| { if let Some(slot) = body.first() { ui.allocate_ui(egui::vec2(left, ui.available_height()), |ui| { scrolled(ui, slot, pass); }); } ui.vertical(|ui| { for slot in body.iter().skip(1) { scrolled(ui, slot, pass); } }); }); } } // One region filling the frame. Down the page, because there is no // division to put anything beside. Arrangement::Single => { for slot in body { scrolled(ui, slot, pass); } } } } /// A pane with its own scroll. /// /// Scroll is egui's here, which is one of the three things `quasi-tui`'s `View` /// holds and this crate's does not. The id is the slot's, so a pane keeps its /// position across frames the way the description's stable ids intend. fn scrolled(ui: &mut Ui, slot: &Slot, pass: &mut Pass<'_>) { egui::ScrollArea::vertical() .id_salt(&slot.id) .show(ui, |ui| { node::region(pass, ui, slot); }); } /// A share as a fraction of the width on offer. fn share_of(share: quasi_router::layout::Share) -> f32 { // `Share::of` answers in whole columns for a terminal. A window has // fractional width, so the ratio is taken against a large denominator and // divided back, which keeps one fact behind both renderers' proportions. const SCALE: u16 = 1000; f32::from(share.of(SCALE)) / f32::from(SCALE) }