use std::collections::HashMap;
use yog_formats::book::Book;
use yog_formats::book::layout::ICON_PX;
use yog_gfx::{Color, Rect, Vec2};
use yog_ui::{Align, FlexDir, UiRoot, widget};
use super::geometry::{
BK_H, BK_W, BODY_Y, BOOK_NAME_X, BOOK_NAME_Y, CAT_CELL, CAT_COLUMNS, CAT_GRID_X, CAT_GRID_Y,
CAT_ICON_INSET, COUNT_H, COUNT_SLACK, EDITION_X, EDITION_Y, GAP, LANDING_FIT_STEP,
LANDING_MIN_FIT, LANDING_Y, LEFT_X, LIST_ICON, LIST_ICON_INSET, LIST_NAME_W, LIST_PAD_BOTTOM,
LIST_ROW_H, LIST_Y, NAMEPLATE_H, NAMEPLATE_V, NAMEPLATE_W, NAMEPLATE_X, NAMEPLATE_Y, PAGE_H,
PAGE_W, RIGHT_X, SEP_H, SEP_Y, TEXT_W, TITLE_H, TOP_PAD,
};
use super::names::{ordinal, word};
use super::overlay::{BgSprite, OverlayCmd, SpriteSheet};
use super::recipe::RecipeVis;
use super::widgets::{Canvas, Nav, fit, lbl, set_text_scale, text_scale, title_lbl};
use crate::state::{BookViewState, action};
use crate::theme::BookTheme;
pub(crate) struct Spread<'a> {
pub book: &'a Book,
pub state: &'a BookViewState,
pub theme: &'a BookTheme,
pub recipes: &'a HashMap<String, RecipeVis>,
pub sheet: &'a str,
}
pub(crate) type Built = (UiRoot, Vec<BgSprite>, Vec<OverlayCmd>);
impl Spread<'_> {
pub(crate) fn build(&self, book_rect: Rect) -> Built {
let (mut sprites, mut overlays) = (Vec::new(), Vec::new());
let (sx, sy) = (book_rect.w / BK_W, book_rect.h / BK_H);
set_text_scale(sx);
let mut c = Canvas {
theme: self.theme,
sheet: self.sheet,
sx,
sy,
sprites: &mut sprites,
overlays: &mut overlays,
};
let page_at = |x: f32| Vec2::new(book_rect.x + x * sx, book_rect.y + TOP_PAD * sy);
let (left, right) = (page_at(LEFT_X), page_at(RIGHT_X));
c.separator(right, SEP_Y);
let (left_page, right_page) = if self.state.at_home {
c.sprites.push(BgSprite {
sheet: SpriteSheet::Book,
src: Rect::new(0.0, NAMEPLATE_V, NAMEPLATE_W, NAMEPLATE_H),
dst: c.rect(
Vec2::new(book_rect.x, book_rect.y),
Rect::new(NAMEPLATE_X, NAMEPLATE_Y, NAMEPLATE_W, NAMEPLATE_H),
),
});
(
self.landing(&mut c, book_rect),
self.categories(&mut c, right),
)
} else {
(self.entry(&mut c, left), self.entries(&mut c, right))
};
let spine_gap = (RIGHT_X - LEFT_X - PAGE_W) * sx;
let pages = widget::panel(FlexDir::Row)
.w(book_rect.w)
.h(book_rect.h)
.gap(0.0)
.padding(TOP_PAD * sy, 0.0, 0.0, LEFT_X * sx)
.child(left_page)
.child(widget::spacer().w(spine_gap))
.child(right_page);
let screen = widget::panel(FlexDir::Row)
.padding(book_rect.y, 0.0, 0.0, book_rect.x)
.child(pages);
(UiRoot::new(&self.book.id, screen), sprites, overlays)
}
fn page(&self, c: &Canvas<'_>) -> widget::Widget {
widget::panel(FlexDir::Column)
.w(PAGE_W * c.sx)
.h(PAGE_H * c.sy)
.gap(0.0)
}
fn landing(&self, c: &mut Canvas<'_>, book_rect: Rect) -> widget::Widget {
let book = self.book;
let on_book = |x: f32, y: f32| Vec2::new(book_rect.x + x * c.sx, book_rect.y + y * c.sy);
c.overlays.push(OverlayCmd::McText {
text: book.name.clone(),
at: on_book(BOOK_NAME_X, BOOK_NAME_Y),
color: c.theme.nameplate,
});
let second = match book.version {
0 => book.subtitle.clone(),
n => Some(word("edition", &ordinal(n))),
};
if let Some(text) = second {
c.overlays.push(OverlayCmd::McText {
text,
at: on_book(EDITION_X, EDITION_Y),
color: c.theme.nameplate,
});
}
let mut col = self
.page(c)
.padding(LANDING_Y * c.sy, PAGE_PAD_RIGHT, GAP, GAP);
let fit = landing_fit(&book.landing_text);
for para in book.landing_text.split('\n') {
col = col.child(match para.is_empty() {
true => widget::spacer().h(GAP * c.sy),
false => lbl(para).color(c.theme.text).font_scale(text_scale() * fit),
});
}
let count = title_lbl(word("entry_count", &book.entries.len().to_string()))
.color(c.theme.nav)
.h(COUNT_H * c.sy)
.align(Align::Center);
col.child(widget::spacer().flex(1.0)).child(count)
}
fn categories(&self, c: &mut Canvas<'_>, page: Vec2) -> widget::Widget {
let header_h = TITLE_H * c.sy;
let mut col = self.page(c).padding(0.0, GAP, GAP, 0.0);
col = col.child(
title_lbl(word("categories", ""))
.color(c.theme.divider)
.h(header_h)
.align(Align::Center)
.no_wrap(),
);
col = col.child(widget::spacer().h(CAT_GRID_Y * c.sy - header_h));
let icon = ICON_PX * c.sx.min(c.sy);
for (row_i, row_cats) in self.book.categories.chunks(CAT_COLUMNS).enumerate() {
let mut row = widget::panel(FlexDir::Row)
.h(CAT_CELL * c.sy)
.gap(0.0)
.padding(0.0, 0.0, 0.0, CAT_GRID_X * c.sx);
for (col_i, cat) in row_cats.iter().enumerate() {
let idx = row_i * CAT_COLUMNS + col_i;
if let Some(icon_id) = &cat.icon {
let cell = Vec2::new(
CAT_GRID_X + col_i as f32 * CAT_CELL + CAT_ICON_INSET,
CAT_GRID_Y + row_i as f32 * CAT_CELL + CAT_ICON_INSET,
);
let at = c.rect(page, Rect::new(cell.x, cell.y, 0.0, 0.0));
c.overlays
.push(OverlayCmd::icon(icon_id, Rect::new(at.x, at.y, icon, icon)));
}
let selected = !self.state.at_home && idx == self.state.cat;
let bg = if selected {
c.theme.nav_selected_bg
} else {
Color::TRANSPARENT
};
let cell = widget::panel(FlexDir::Column)
.w(CAT_CELL * c.sx)
.h(CAT_CELL * c.sy)
.bg(bg)
.on_click(action::category(idx))
.id(format!("book_cat_{idx}"));
row = row.child(cell);
}
col = col.child(row);
}
col
}
fn entry(&self, c: &mut Canvas<'_>, page: Vec2) -> widget::Widget {
let entry = self.state.current_entry(self.book);
let shown = entry.and_then(|e| e.pages.get(self.state.page));
c.separator(page, SEP_Y);
let nav = c.nav(Nav {
prev: action::PREV_PAGE,
next: action::NEXT_PAGE,
label: format!(
"{}/{}",
self.state.page + 1,
self.state.page_count(self.book)
),
home: Some(action::HOME),
});
let body = Vec2::new(page.x, page.y + BODY_Y * c.sy);
let content = super::page::build(
c,
&super::page::Context {
recipes: self.recipes,
book: self.book,
number: self.state.page,
at: body,
},
shown,
);
let title = entry.map(|e| e.name.as_str()).unwrap_or("");
let sep_h = c.sep_h();
self.page(c)
.padding(0.0, PAGE_PAD_RIGHT, GAP, GAP)
.child(
title_lbl(title)
.color(c.theme.title)
.h(TITLE_H * c.sy)
.align(Align::Center)
.no_wrap(),
)
.child(widget::spacer().h((SEP_Y - TITLE_H) * c.sy))
.child(widget::spacer().h(sep_h))
.child(widget::spacer().h((BODY_Y - SEP_Y - SEP_H) * c.sy))
.child(content)
.child(nav)
}
fn entries(&self, c: &mut Canvas<'_>, page: Vec2) -> widget::Widget {
let book = self.book;
let cat_name = book
.categories
.get(self.state.cat)
.map(|c| c.name.clone())
.unwrap_or_else(|| word("entries", ""));
let icon = LIST_ICON * c.sx.min(c.sy);
let mut col = self.page(c).padding(0.0, GAP, LIST_PAD_BOTTOM, 0.0);
col = col.child(
title_lbl(cat_name)
.color(c.theme.divider)
.h(TITLE_H * c.sy)
.align(Align::Center),
);
col = col.child(widget::spacer().h((SEP_Y - TITLE_H) * c.sy));
col = col.child(widget::spacer().h(c.sep_h()));
col = col.child(widget::spacer().h((LIST_Y - SEP_Y - SEP_H) * c.sy));
let start = self.state.list_spread_start();
for (i, entry) in self.state.entries_visible(book).iter().enumerate() {
let abs_i = start + i;
let selected = abs_i == self.state.entry;
if let Some(icon_id) = &entry.icon {
let at = c.rect(
page,
Rect::new(
LIST_ICON_INSET,
LIST_Y + i as f32 * LIST_ROW_H + LIST_ICON_INSET,
0.0,
0.0,
),
);
c.overlays
.push(OverlayCmd::icon(icon_id, Rect::new(at.x, at.y, icon, icon)));
}
let (bg, color) = if selected {
(c.theme.nav_selected_bg, c.theme.nav_selected)
} else {
(Color::TRANSPARENT, c.theme.nav)
};
let name = lbl(&entry.name)
.color(color)
.font_scale(text_scale() * fit(&entry.name, LIST_NAME_W))
.flex(1.0)
.no_wrap();
let row = widget::panel(FlexDir::Row)
.h(LIST_ROW_H * c.sy)
.gap(LIST_GAP)
.bg(bg)
.on_click(action::entry(abs_i))
.id(format!("book_entry_{abs_i}"))
.padding(LIST_ICON_INSET, LIST_GAP, LIST_ICON_INSET, LIST_GAP)
.child(widget::spacer().w(icon + LIST_GAP))
.child(name);
col = col.child(row);
}
let spreads = self.state.list_spread_count(book);
if spreads > 1 {
let label = format!("{}/{}", self.state.list_spread + 1, spreads);
col = col.child(widget::spacer().flex(1.0)).child(c.nav(Nav {
prev: action::PREV_LIST,
next: action::NEXT_LIST,
label,
home: None,
}));
}
col
}
}
const PAGE_PAD_RIGHT: f32 = 6.0;
const LIST_GAP: f32 = 2.0;
pub(super) fn landing_fit(text: &str) -> f32 {
use yog_ui::text::{LINE_GAP, LINE_H, line_count};
let room = PAGE_H - LANDING_Y - GAP - COUNT_H - COUNT_SLACK;
let height = |scale: f32| -> f32 {
text.split('\n')
.map(|p| match p.is_empty() {
true => GAP,
false => line_count(p, TEXT_W, scale) as f32 * (LINE_H * scale + LINE_GAP),
})
.sum()
};
let mut scale = 1.0;
while scale > LANDING_MIN_FIT && height(scale) > room {
scale -= LANDING_FIT_STEP;
}
scale
}