use crate::{COMPACT_WIDTH, icons, tr};
const COLUMN_WIDTH: f32 = 320.0;
const COLUMN_MIN_WIDTH: f32 = 240.0;
const MAIN_MIN_WIDTH: f32 = 360.0;
const CARD_MAX_WIDTH: f32 = 560.0;
const CARD_SHARE: f32 = 0.8;
#[derive(Clone, Copy, Debug)]
struct Offer {
pane: egui::Id,
column: bool,
pass: u64,
}
fn offer_id() -> egui::Id {
egui::Id::unique("corundum_detail_offer")
}
fn clicked_id() -> egui::Id {
egui::Id::unique("corundum_detail_clicked")
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum Part {
Main,
Detail,
}
#[cfg(test)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Shown {
Not,
Column,
Card,
Screen,
}
#[derive(Clone, Debug)]
pub struct DetailPane {
id: egui::Id,
column: bool,
open: bool,
#[cfg(test)]
shown: Shown,
}
impl DetailPane {
pub fn new(id_salt: impl egui::AsIdSalt) -> DetailPane {
DetailPane {
id: egui::Id::unique(("corundum_detail", id_salt)),
column: true,
open: false,
#[cfg(test)]
shown: Shown::Not,
}
}
pub fn with_column(mut self, on: bool) -> DetailPane {
self.column = on;
self
}
pub fn column(&self) -> bool {
self.column
}
pub fn set_column(&mut self, on: bool) {
self.column = on;
}
pub fn open(&mut self) {
self.open = true;
}
pub fn close(&mut self) {
self.open = false;
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn ui(
&mut self,
ui: &mut egui::Ui,
main: impl FnOnce(&mut egui::Ui),
detail: impl FnOnce(&mut egui::Ui),
) {
let (mut main, mut detail) = (Some(main), Some(detail));
self.show(ui, |ui, part| match part {
Part::Main => main.take().map_or((), |m| m(ui)),
Part::Detail => detail.take().map_or((), |d| d(ui)),
});
}
pub(crate) fn show(&mut self, ui: &mut egui::Ui, mut draw: impl FnMut(&mut egui::Ui, Part)) {
if ui.ctx().content_rect().width() < COMPACT_WIDTH {
self.screen_ui(ui, &mut draw);
return;
}
let room = ui.available_width();
let fits = room >= MAIN_MIN_WIDTH + COLUMN_WIDTH;
if fits {
self.offer_toggle(ui.ctx());
}
let column = self.column && fits;
if column {
self.open = false;
egui::Panel::right(self.id.with("column"))
.resizable(true)
.default_size(COLUMN_WIDTH)
.min_size(COLUMN_MIN_WIDTH)
.max_size(room - MAIN_MIN_WIDTH)
.show(ui, |ui| {
egui::ScrollArea::vertical()
.id_salt(self.id.with("column_scroll"))
.auto_shrink(false)
.show(ui, |ui| draw(ui, Part::Detail));
});
self.set_shown(true, false);
}
draw(ui, Part::Main);
if !column {
self.set_shown(false, self.open);
if self.open {
self.card_ui(ui.ctx(), &mut draw);
}
}
}
fn screen_ui(&mut self, ui: &mut egui::Ui, draw: &mut impl FnMut(&mut egui::Ui, Part)) {
#[cfg(test)]
{
self.shown = if self.open { Shown::Screen } else { Shown::Not };
}
if !self.open {
draw(ui, Part::Main);
return;
}
egui::CentralPanel::default().show(ui, |ui| {
let back = icons::image(ui, icons::arrow_left(), 14.0);
if ui
.add(egui::Button::image_and_text(
back,
tr!(ui, "corundum-detail-back"),
))
.clicked()
{
self.open = false;
}
ui.separator();
egui::ScrollArea::vertical()
.id_salt(self.id.with("screen_scroll"))
.auto_shrink(false)
.show(ui, |ui| draw(ui, Part::Detail));
});
}
fn offer_toggle(&mut self, ctx: &egui::Context) {
let clicked = ctx.data_mut(|d| {
let ours = d.get_temp::<egui::Id>(clicked_id()) == Some(self.id);
if ours {
d.remove::<egui::Id>(clicked_id());
}
ours
});
if clicked {
self.column = !self.column;
}
let offer = Offer {
pane: self.id,
column: self.column,
pass: ctx.cumulative_pass_nr(),
};
ctx.data_mut(|d| d.insert_temp(offer_id(), offer));
}
fn card_ui(&mut self, ctx: &egui::Context, draw: &mut impl FnMut(&mut egui::Ui, Part)) {
let screen = ctx.content_rect();
let mut close = false;
let response = egui::Modal::new(self.id.with("card")).show(ctx, |ui| {
ui.set_width((screen.width() * CARD_SHARE).min(CARD_MAX_WIDTH));
egui::Sides::new().show(
ui,
|_| {},
|ui| {
close = ui
.add(icons::button(icons::x(), 14.0))
.on_hover_text(tr!(ui, "corundum-detail-close"))
.clicked();
},
);
egui::ScrollArea::vertical()
.id_salt(self.id.with("card_scroll"))
.max_height(screen.height() * CARD_SHARE)
.show(ui, |ui| draw(ui, Part::Detail));
});
if close || response.should_close() {
self.open = false;
}
}
#[cfg_attr(not(test), allow(unused_variables))]
fn set_shown(&mut self, column: bool, card: bool) {
#[cfg(test)]
{
self.shown = match (column, card) {
(true, _) => Shown::Column,
(false, true) => Shown::Card,
(false, false) => Shown::Not,
};
}
}
}
pub fn toggle_ui(ui: &mut egui::Ui) -> Option<egui::Response> {
let offer = ui.ctx().data(|d| d.get_temp::<Offer>(offer_id()))?;
if offer.pass + 1 < ui.ctx().cumulative_pass_nr() {
return None;
}
let hover = if offer.column {
"corundum-detail-hide"
} else {
"corundum-detail-show"
};
let response = ui
.add(icons::button(icons::panel_right(), 14.0).selected(offer.column))
.on_hover_text(tr!(ui, hover));
if response.clicked() {
ui.ctx()
.data_mut(|d| d.insert_temp(clicked_id(), offer.pane));
ui.ctx().request_repaint();
}
Some(response)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{render, render_in};
const WIDE: [f32; 2] = [1000.0, 700.0];
const NARROW_DESKTOP: [f32; 2] = [640.0, 700.0];
const PHONE: [f32; 2] = [390.0, 844.0];
fn drawn(pane: &mut DetailPane, size: [f32; 2]) -> (bool, bool) {
let (mut main, mut detail) = (false, false);
render(size, |ui| {
pane.ui(ui, |_| main = true, |_| detail = true);
});
(main, detail)
}
#[test]
fn wide_the_column_shows_the_detail_beside_the_main_view() {
let mut pane = DetailPane::new("t");
assert_eq!(drawn(&mut pane, WIDE), (true, true));
assert_eq!(pane.shown, Shown::Column);
}
#[test]
fn a_main_view_with_its_own_panels_keeps_clear_of_the_column() {
let ctx = egui::Context::default();
let mut pane = DetailPane::new("t");
let (mut list, mut page, mut column) = (
egui::Rect::NOTHING,
egui::Rect::NOTHING,
egui::Rect::NOTHING,
);
render_in(&ctx, WIDE, |ui| {
pane.ui(
ui,
|ui| {
list = egui::Panel::left("t_list")
.show(ui, |ui| ui.label("list"))
.response
.rect;
page = egui::CentralPanel::default()
.show(ui, |ui| ui.label("page"))
.response
.rect;
},
|ui| column = ui.max_rect(),
)
});
assert!(list.right() <= page.left(), "{list:?} {page:?}");
assert!(page.right() <= column.left(), "{page:?} {column:?}");
}
#[test]
fn opening_while_the_column_shows_leaves_nothing_to_pop_up() {
let mut pane = DetailPane::new("t");
pane.open();
drawn(&mut pane, WIDE);
assert!(!pane.is_open());
pane.set_column(false);
assert_eq!(drawn(&mut pane, WIDE), (true, false));
assert_eq!(pane.shown, Shown::Not);
}
#[test]
fn without_the_column_an_opened_detail_is_a_card() {
let mut pane = DetailPane::new("t").with_column(false);
assert_eq!(drawn(&mut pane, WIDE), (true, false));
pane.open();
assert_eq!(drawn(&mut pane, WIDE), (true, true));
assert_eq!(pane.shown, Shown::Card);
pane.close();
assert_eq!(drawn(&mut pane, WIDE), (true, false));
}
#[test]
fn the_card_is_as_tall_as_its_detail() {
let ctx = egui::Context::default();
let mut pane = DetailPane::new("t").with_column(false);
pane.open();
render_in(&ctx, WIDE, |ui| {
pane.ui(
ui,
|_| {},
|ui| {
ui.label("one line");
},
)
});
let card = ctx
.memory(|m| m.area_rect(pane.id.with("card")))
.expect("the card is drawn");
assert!(card.height() < WIDE[1] / 4.0, "{card:?}");
assert!(card.width() <= CARD_MAX_WIDTH + 32.0, "{card:?}");
}
#[test]
fn with_no_room_for_the_column_the_detail_is_a_card() {
let mut pane = DetailPane::new("t");
assert_eq!(drawn(&mut pane, NARROW_DESKTOP), (true, false));
pane.open();
assert_eq!(drawn(&mut pane, NARROW_DESKTOP), (true, true));
assert_eq!(pane.shown, Shown::Card);
assert!(pane.column(), "the column stays wanted, for a wider window");
}
#[test]
fn on_a_phone_the_detail_takes_the_main_views_place() {
let mut pane = DetailPane::new("t");
assert_eq!(drawn(&mut pane, PHONE), (true, false));
pane.open();
assert_eq!(drawn(&mut pane, PHONE), (false, true));
assert_eq!(pane.shown, Shown::Screen);
}
fn click_toggle(pane: &mut DetailPane, size: [f32; 2]) -> bool {
let ctx = egui::Context::default();
let frame = |events: Vec<egui::Event>, pane: &mut DetailPane| {
let mut drawn = None;
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(size[0], size[1]),
)),
events,
..Default::default()
};
let mut output = ctx.run_ui(input, |ui| {
pane.ui(ui, |_| {}, |_| {});
let corner =
egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(40.0, 40.0));
let response = ui
.scope_builder(egui::UiBuilder::new().max_rect(corner), toggle_ui)
.inner;
drawn = response.map(|r| r.rect);
});
output.textures_delta.clear();
drawn
};
frame(vec![], pane);
let Some(rect) = frame(vec![], pane) else {
return false;
};
let at = rect.center();
let press = |pressed| egui::Event::PointerButton {
pos: at,
button: egui::PointerButton::Primary,
pressed,
modifiers: egui::Modifiers::NONE,
};
frame(vec![egui::Event::PointerMoved(at), press(true)], pane);
frame(vec![press(false)], pane);
frame(vec![], pane);
true
}
#[test]
fn the_toggle_turns_the_column_off_and_on() {
let mut pane = DetailPane::new("t");
assert!(click_toggle(&mut pane, WIDE));
assert!(!pane.column());
assert!(click_toggle(&mut pane, WIDE));
assert!(pane.column());
}
#[test]
fn without_room_for_the_column_there_is_no_toggle() {
for size in [NARROW_DESKTOP, PHONE] {
let mut pane = DetailPane::new("t");
assert!(!click_toggle(&mut pane, size), "{size:?}");
assert!(pane.column());
}
}
}