//! 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 same three findings `quasi-tui`'s `region` module records apply here, //! unchanged**, because they are about the description rather than about //! terminals. Repeated rather than cross-referenced only, since a reader here //! needs to know the guesses are guesses: //! //! - **A tabbed arrangement does not say which tab is showing.** This draws the //! first, which is a guess. //! - **A tab has no label.** [`Slot::id`] is an address chosen to be stable for //! fragment targeting, so using it as a heading puts `contacts-detail` on //! screen. //! - **A region's share is [`Arrangement::share`] and nothing finer.** //! //! 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. pub(crate) fn screen_regions(pass: &mut Pass<'_>, ui: &mut Ui, screen: &Screen) { // Bands stack at the top, full width, in the order they were said. A band is // an arrangement rather than a type -- a page header, a toolbar -- so it // takes the height it needs and gets out of the way. for slot in screen .slots .iter() .filter(|slot| matches!(slot.kind, RegionKind::Band)) { node::region(pass, ui, slot); } let body: Vec<&Slot> = screen .slots .iter() .filter(|slot| !matches!(slot.kind, RegionKind::Band)) .collect(); 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); } }); }); } } } } /// 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) }