mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 17:42:20 +00:00
feat(menu): inset the graphical menu in a panel with the logo above it
Entries now sit in a centered panel instead of spanning the screen, and only the selected entry gets a pill. The logo and name are in a header above the panel and always show. Long titles end in an ellipsis, and a scrollbar and position show when the list doesn't fit. Text scale is capped so 1440p displays aren't dominated by the titles.
This commit is contained in:
1 parent
1bebcd2ecc
commit
27d2a5c56e
2 files changed
+264
-71
No files matched your search
@@ -4,6 +4,9 @@ pub const GLYPH_SIZE: usize = 8;
|
|||||||
/// A glyph as eight rows from top to bottom, where the lowest bit of a row is its leftmost pixel.
|
/// A glyph as eight rows from top to bottom, where the lowest bit of a row is its leftmost pixel.
|
||||||
pub type Glyph = [u8; GLYPH_SIZE];
|
pub type Glyph = [u8; GLYPH_SIZE];
|
||||||
|
|
||||||
|
/// Three dots, which are drawn where a title is cut off.
|
||||||
|
pub const ELLIPSIS: Glyph = [0x00, 0x00, 0x00, 0x00, 0x00, 0xDB, 0xDB, 0x00];
|
||||||
|
|
||||||
/// A heart, which is drawn next to the selected entry.
|
/// A heart, which is drawn next to the selected entry.
|
||||||
pub const HEART: Glyph = [0x00, 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18];
|
pub const HEART: Glyph = [0x00, 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18];
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::entries::BootableEntry;
|
use crate::entries::BootableEntry;
|
||||||
use crate::menu::font::{self, GLYPH_SIZE, Glyph, HEART};
|
use crate::menu::font::{self, ELLIPSIS, GLYPH_SIZE, Glyph, HEART};
|
||||||
use crate::menu::logo::{LOGO, LOGO_HEIGHT, LOGO_WIDTH};
|
use crate::menu::logo::{LOGO, LOGO_HEIGHT, LOGO_WIDTH};
|
||||||
use crate::menu::{BootMenu, wait_for_events};
|
use crate::menu::{BootMenu, wait_for_events};
|
||||||
use alloc::format;
|
use alloc::format;
|
||||||
@@ -37,8 +37,14 @@ const BACKGROUND_TOP: (u8, u8, u8) = (0x3a, 0x1c, 0x4e);
|
|||||||
/// The color of the bottom of the background.
|
/// The color of the bottom of the background.
|
||||||
const BACKGROUND_BOTTOM: (u8, u8, u8) = (0x16, 0x0c, 0x24);
|
const BACKGROUND_BOTTOM: (u8, u8, u8) = (0x16, 0x0c, 0x24);
|
||||||
|
|
||||||
/// The color of the pill behind each entry that isn't selected.
|
/// The widest that the status line gets, which the panel has room for.
|
||||||
const PILL: BltPixel = BltPixel::new(0x2c, 0x1a, 0x40);
|
const STATUS_WIDEST: &str = "Boot in 999 s.";
|
||||||
|
|
||||||
|
/// The color of the panel behind the entries.
|
||||||
|
const PANEL: BltPixel = BltPixel::new(0x22, 0x12, 0x34);
|
||||||
|
|
||||||
|
/// The color of the edge of the panel, the line below the entries and the scrollbar track.
|
||||||
|
const PANEL_EDGE: BltPixel = BltPixel::new(0x4a, 0x2e, 0x66);
|
||||||
|
|
||||||
/// The color of the pill behind the selected entry, taken from the Sprout logo.
|
/// The color of the pill behind the selected entry, taken from the Sprout logo.
|
||||||
const PILL_SELECTED: BltPixel = BltPixel::new(0xff, 0x7a, 0xb8);
|
const PILL_SELECTED: BltPixel = BltPixel::new(0xff, 0x7a, 0xb8);
|
||||||
@@ -208,7 +214,8 @@ impl Mouse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The position and size of the entries on the framebuffer.
|
/// The position and size of everything on the framebuffer.
|
||||||
|
/// The logo and the name are in a header, with a panel below it that holds the entries.
|
||||||
struct Layout {
|
struct Layout {
|
||||||
/// How many pixels each pixel of the font is drawn as.
|
/// How many pixels each pixel of the font is drawn as.
|
||||||
scale: usize,
|
scale: usize,
|
||||||
@@ -226,39 +233,118 @@ struct Layout {
|
|||||||
gap: usize,
|
gap: usize,
|
||||||
/// The number of entries that fit on the framebuffer.
|
/// The number of entries that fit on the framebuffer.
|
||||||
visible: usize,
|
visible: usize,
|
||||||
|
/// The column, row, width and height of the panel behind the entries.
|
||||||
|
panel: (usize, usize, usize, usize),
|
||||||
|
/// How round the corners of the panel are.
|
||||||
|
radius: usize,
|
||||||
|
/// The column that the text of an entry starts at.
|
||||||
|
text_x: usize,
|
||||||
|
/// How many characters of a title fit in an entry.
|
||||||
|
columns: usize,
|
||||||
|
/// The row of the line between the entries and the status line.
|
||||||
|
footer: usize,
|
||||||
|
/// The column, row, width and height of the logo when it is on the ground.
|
||||||
|
logo: (usize, usize, usize, usize),
|
||||||
|
/// How many pixels the logo rises while it bounces.
|
||||||
|
bounce: usize,
|
||||||
|
/// The column, row and scale of the name, if there is room for it.
|
||||||
|
title: Option<(usize, usize, usize)>,
|
||||||
|
/// Whether there are more entries than fit, which adds a scrollbar.
|
||||||
|
scrolls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
/// Calculate the layout of `entries` on a framebuffer of `screen` width and height.
|
/// Calculate the layout of `entries` on a framebuffer of `screen` width and height.
|
||||||
fn new(screen: (usize, usize), entries: &[BootableEntry]) -> Self {
|
fn new(screen: (usize, usize), entries: &[BootableEntry]) -> Self {
|
||||||
// Large screens get larger text so that it stays readable.
|
let (width, height) = screen;
|
||||||
let scale = (screen.1 / 360).clamp(1, 4);
|
|
||||||
let cell = GLYPH_SIZE * scale;
|
|
||||||
let (height, gap) = (cell * 2, cell / 2);
|
|
||||||
|
|
||||||
// All entries are the width of the longest title, with room for the heart and padding.
|
// Large screens get larger text so that it stays readable, but not so large that the
|
||||||
|
// titles take up the whole screen.
|
||||||
|
let scale = (height / 540).min(width / 640).clamp(1, 3);
|
||||||
|
let cell = GLYPH_SIZE * scale;
|
||||||
|
let margin = (cell * 2).max(height / 20).min(width / 8);
|
||||||
|
let padding = cell * 3 / 2;
|
||||||
|
let (row, gap) = (cell * 2, scale * 2);
|
||||||
|
let header_gap = cell * 2;
|
||||||
|
|
||||||
|
// The line between the entries and the status, and the status below it.
|
||||||
|
let footer_height = cell + scale + cell / 2 + cell;
|
||||||
|
|
||||||
|
// The panel is the width of the longest title, but never most of the screen.
|
||||||
let longest = entries
|
let longest = entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.title().chars().count())
|
.map(|entry| entry.title().chars().count())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let width = ((longest + 4) * cell).min(screen.0.saturating_sub(cell * 2));
|
let status = HINT.len().max(STATUS_WIDEST.len());
|
||||||
|
let panel_width = ((longest + 4) * cell + padding * 2)
|
||||||
|
.max(40 * cell)
|
||||||
|
.max((status + 8) * cell)
|
||||||
|
.min(width.saturating_sub(margin * 2).min(width * 3 / 5));
|
||||||
|
|
||||||
// The title is above the entries, and the status line is below.
|
// The header is as tall as the logo, and the logo needs room to bounce.
|
||||||
let top = cell * 4;
|
let fit = |logo: usize| {
|
||||||
let space = screen.1.saturating_sub(top + cell * 3);
|
let header = logo + logo / 16;
|
||||||
let visible = entries.len().min(((space + gap) / (height + gap)).max(1));
|
let space = height
|
||||||
let list = visible * (height + gap) - gap;
|
.saturating_sub(margin * 2 + header + header_gap + padding * 2 + footer_height);
|
||||||
|
let visible = ((space + gap) / (row + gap)).clamp(1, entries.len().max(1));
|
||||||
|
(header, visible)
|
||||||
|
};
|
||||||
|
let mut logo_height = (LOGO_HEIGHT * 2).min(height / 7).max(32);
|
||||||
|
let (mut header, mut visible) = fit(logo_height);
|
||||||
|
// A short screen gets a smaller logo before it loses entries.
|
||||||
|
if visible < entries.len().min(3) {
|
||||||
|
logo_height = (logo_height / 2).max(32);
|
||||||
|
(header, visible) = fit(logo_height);
|
||||||
|
}
|
||||||
|
let logo_width = logo_height * LOGO_WIDTH / LOGO_HEIGHT;
|
||||||
|
let bounce = logo_height / 16;
|
||||||
|
|
||||||
|
// The name is next to the logo, if the two fit together.
|
||||||
|
let title_scale = (logo_height / 32).clamp(scale, scale * 3);
|
||||||
|
let title_width = TITLE.len() * GLYPH_SIZE * title_scale;
|
||||||
|
let fits = logo_width + cell + title_width <= width.saturating_sub(margin * 2);
|
||||||
|
let group = if fits {
|
||||||
|
logo_width + cell + title_width
|
||||||
|
} else {
|
||||||
|
logo_width
|
||||||
|
};
|
||||||
|
let group_x = width.saturating_sub(group) / 2;
|
||||||
|
|
||||||
|
let list = visible * (row + gap) - gap;
|
||||||
|
let panel_height = padding * 2 + list + footer_height;
|
||||||
|
let top = (height.saturating_sub(header + header_gap + panel_height) / 2).max(margin);
|
||||||
|
let panel_x = width.saturating_sub(panel_width) / 2;
|
||||||
|
let panel_y = top + header + header_gap;
|
||||||
|
|
||||||
|
// The scrollbar is in a column next to the entries.
|
||||||
|
let scrolls = visible < entries.len();
|
||||||
|
let gutter = if scrolls { cell } else { 0 };
|
||||||
|
let list_width = panel_width.saturating_sub(padding * 2 + gutter);
|
||||||
|
let list_y = panel_y + padding;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
scale,
|
scale,
|
||||||
cell,
|
cell,
|
||||||
x: (screen.0 - width) / 2,
|
x: panel_x + padding,
|
||||||
y: top + space.saturating_sub(list) / 2,
|
y: list_y,
|
||||||
width,
|
width: list_width,
|
||||||
height,
|
height: row,
|
||||||
gap,
|
gap,
|
||||||
visible,
|
visible,
|
||||||
|
panel: (panel_x, panel_y, panel_width, panel_height),
|
||||||
|
radius: cell,
|
||||||
|
text_x: panel_x + padding + cell * 3,
|
||||||
|
columns: list_width.saturating_sub(cell * 4) / cell,
|
||||||
|
footer: list_y + list + cell,
|
||||||
|
logo: (group_x, top + bounce, logo_width, logo_height),
|
||||||
|
bounce,
|
||||||
|
title: fits.then_some((
|
||||||
|
group_x + logo_width + cell,
|
||||||
|
top + bounce + logo_height.saturating_sub(GLYPH_SIZE * title_scale) / 2,
|
||||||
|
title_scale,
|
||||||
|
)),
|
||||||
|
scrolls,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,18 +484,22 @@ fn draw_background(fb: &mut Framebuffer, top: (u8, u8, u8), bottom: (u8, u8, u8)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fill a rectangle with the shape of a pill, which has fully rounded ends.
|
/// Fill a rectangle that has rounded corners of `radius`.
|
||||||
fn draw_pill(
|
fn draw_rounded(
|
||||||
fb: &mut Framebuffer,
|
fb: &mut Framebuffer,
|
||||||
x: usize,
|
x: usize,
|
||||||
y: usize,
|
y: usize,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
|
radius: usize,
|
||||||
color: BltPixel,
|
color: BltPixel,
|
||||||
) {
|
) {
|
||||||
let radius = height / 2;
|
if width == 0 || height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let radius = radius.min(height / 2).min(width / 2);
|
||||||
for row in 0..height {
|
for row in 0..height {
|
||||||
// How far the row is from the edge, where the ends curve in.
|
// How far the row is from the edge, where the corners curve in.
|
||||||
let edge = row.min(height - 1 - row);
|
let edge = row.min(height - 1 - row);
|
||||||
let inset = if edge >= radius {
|
let inset = if edge >= radius {
|
||||||
0
|
0
|
||||||
@@ -429,6 +519,18 @@ fn draw_pill(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fill a rectangle with the shape of a pill, which has fully rounded ends.
|
||||||
|
fn draw_pill(
|
||||||
|
fb: &mut Framebuffer,
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
color: BltPixel,
|
||||||
|
) {
|
||||||
|
draw_rounded(fb, x, y, width, height, height / 2, color);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw `glyph` at `x` and `y`, with each pixel of it drawn as `scale` by `scale` pixels.
|
/// Draw `glyph` at `x` and `y`, with each pixel of it drawn as `scale` by `scale` pixels.
|
||||||
fn draw_glyph(
|
fn draw_glyph(
|
||||||
fb: &mut Framebuffer,
|
fb: &mut Framebuffer,
|
||||||
@@ -445,7 +547,7 @@ fn draw_glyph(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw `text` starting at `x` and `y`, cut off to at most `columns` characters.
|
/// Draw `text` starting at `x` and `y`, cut off with an ellipsis if it is over `columns` characters.
|
||||||
fn draw_text(
|
fn draw_text(
|
||||||
fb: &mut Framebuffer,
|
fb: &mut Framebuffer,
|
||||||
x: usize,
|
x: usize,
|
||||||
@@ -455,7 +557,13 @@ fn draw_text(
|
|||||||
text: &str,
|
text: &str,
|
||||||
color: BltPixel,
|
color: BltPixel,
|
||||||
) {
|
) {
|
||||||
for (index, c) in text.chars().take(columns).enumerate() {
|
let cut = text.chars().count() > columns;
|
||||||
|
let shown = if cut {
|
||||||
|
columns.saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
columns
|
||||||
|
};
|
||||||
|
for (index, c) in text.chars().take(shown).enumerate() {
|
||||||
draw_glyph(
|
draw_glyph(
|
||||||
fb,
|
fb,
|
||||||
x + index * GLYPH_SIZE * scale,
|
x + index * GLYPH_SIZE * scale,
|
||||||
@@ -465,43 +573,52 @@ fn draw_text(
|
|||||||
color,
|
color,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
if cut && columns > 0 {
|
||||||
|
draw_glyph(
|
||||||
/// Draw `text` in the middle of the framebuffer at `y`.
|
fb,
|
||||||
fn draw_centered(fb: &mut Framebuffer, y: usize, scale: usize, text: &str, color: BltPixel) {
|
x + shown * GLYPH_SIZE * scale,
|
||||||
let columns = fb.width() / (GLYPH_SIZE * scale);
|
y,
|
||||||
let width = text.chars().count().min(columns) * GLYPH_SIZE * scale;
|
scale,
|
||||||
draw_text(fb, (fb.width() - width) / 2, y, scale, columns, text, color);
|
&ELLIPSIS,
|
||||||
}
|
color,
|
||||||
|
);
|
||||||
/// Draw the logo in the bottom right corner, bouncing a little with each of `ticks`.
|
|
||||||
/// Nothing is drawn if the logo would be in the way of the entries.
|
|
||||||
fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) {
|
|
||||||
let factor = layout.scale.div_ceil(2);
|
|
||||||
let (width, height) = (LOGO_WIDTH * factor, LOGO_HEIGHT * factor);
|
|
||||||
let (Some(x), Some(floor)) = (
|
|
||||||
fb.width().checked_sub(width + layout.cell),
|
|
||||||
fb.height().checked_sub(height + layout.cell),
|
|
||||||
) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The entries are in the way if the logo reaches over them, even while it is bouncing up.
|
|
||||||
let reach = layout.row(layout.visible);
|
|
||||||
if x < layout.x + layout.width && floor.saturating_sub(layout.cell) < reach {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The logo rises and falls by up to four logo pixels.
|
/// Draw `text` in the middle of `x` and `width` at `y`.
|
||||||
|
fn draw_centered(
|
||||||
|
fb: &mut Framebuffer,
|
||||||
|
x: usize,
|
||||||
|
width: usize,
|
||||||
|
y: usize,
|
||||||
|
scale: usize,
|
||||||
|
text: &str,
|
||||||
|
color: BltPixel,
|
||||||
|
) {
|
||||||
|
let columns = width / (GLYPH_SIZE * scale);
|
||||||
|
let used = text.chars().count().min(columns) * GLYPH_SIZE * scale;
|
||||||
|
draw_text(fb, x + (width - used) / 2, y, scale, columns, text, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the logo above the entries, bouncing a little with each of `ticks`.
|
||||||
|
fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) {
|
||||||
|
let (x, floor, width, height) = layout.logo;
|
||||||
|
|
||||||
|
// The logo rises and falls by up to `bounce` pixels.
|
||||||
let phase = ticks % BOUNCE_TICKS;
|
let phase = ticks % BOUNCE_TICKS;
|
||||||
let y = floor - phase.min(BOUNCE_TICKS - phase) / 2 * factor;
|
let rise = phase.min(BOUNCE_TICKS - phase) * layout.bounce / (BOUNCE_TICKS / 2);
|
||||||
|
let y = floor - rise;
|
||||||
|
|
||||||
for row in 0..height {
|
for row in 0..height {
|
||||||
for column in 0..width {
|
for column in 0..width {
|
||||||
let source = ((row / factor) * LOGO_WIDTH + column / factor) * 4;
|
let source =
|
||||||
|
((row * LOGO_HEIGHT / height) * LOGO_WIDTH + column * LOGO_WIDTH / width) * 4;
|
||||||
let [red, green, blue, alpha] = LOGO[source..source + 4] else {
|
let [red, green, blue, alpha] = LOGO[source..source + 4] else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if alpha == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// The channels are premultiplied, so the color is the logo plus what shows through.
|
// The channels are premultiplied, so the color is the logo plus what shows through.
|
||||||
if let Some(pixel) = fb.pixel(x + column, y + row) {
|
if let Some(pixel) = fb.pixel(x + column, y + row) {
|
||||||
let through = 255 - alpha as u32;
|
let through = 255 - alpha as u32;
|
||||||
@@ -517,9 +634,23 @@ fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw the track and the thumb of the scrollbar, which shows where `offset` is in `total`.
|
||||||
|
fn draw_scrollbar(fb: &mut Framebuffer, layout: &Layout, offset: usize, total: usize) {
|
||||||
|
let height = layout.visible * (layout.height + layout.gap) - layout.gap;
|
||||||
|
let x = layout.x + layout.width + layout.cell / 2 - layout.scale;
|
||||||
|
let width = layout.scale * 2;
|
||||||
|
fb.fill_rect(x, layout.y, width, height, PANEL_EDGE);
|
||||||
|
|
||||||
|
let thumb = (height * layout.visible / total)
|
||||||
|
.max(layout.cell)
|
||||||
|
.min(height);
|
||||||
|
let travel = total.saturating_sub(layout.visible).max(1);
|
||||||
|
let y = layout.y + (height - thumb) * offset.min(travel) / travel;
|
||||||
|
draw_rounded(fb, x, y, width, thumb, width / 2, ACCENT);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw the cursor with its tip at `position`.
|
/// Draw the cursor with its tip at `position`.
|
||||||
fn draw_cursor(fb: &mut Framebuffer, scale: usize, position: (usize, usize)) {
|
fn draw_cursor(fb: &mut Framebuffer, scale: usize, position: (usize, usize)) {
|
||||||
let scale = scale.div_ceil(2);
|
|
||||||
for (row, line) in CURSOR.iter().enumerate() {
|
for (row, line) in CURSOR.iter().enumerate() {
|
||||||
for (column, shape) in line.bytes().enumerate() {
|
for (column, shape) in line.bytes().enumerate() {
|
||||||
let color = match shape {
|
let color = match shape {
|
||||||
@@ -547,7 +678,31 @@ fn render(
|
|||||||
cursor: Option<(usize, usize)>,
|
cursor: Option<(usize, usize)>,
|
||||||
) {
|
) {
|
||||||
draw_background(fb, BACKGROUND_TOP, BACKGROUND_BOTTOM);
|
draw_background(fb, BACKGROUND_TOP, BACKGROUND_BOTTOM);
|
||||||
draw_centered(fb, layout.cell * 2, layout.scale * 2, TITLE, ACCENT);
|
draw_logo(fb, layout, scene.ticks);
|
||||||
|
if let Some((x, y, scale)) = layout.title {
|
||||||
|
draw_text(fb, x, y, scale, TITLE.len(), TITLE, ACCENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (panel_x, panel_y, panel_width, panel_height) = layout.panel;
|
||||||
|
let edge = layout.scale;
|
||||||
|
draw_rounded(
|
||||||
|
fb,
|
||||||
|
panel_x,
|
||||||
|
panel_y,
|
||||||
|
panel_width,
|
||||||
|
panel_height,
|
||||||
|
layout.radius,
|
||||||
|
PANEL_EDGE,
|
||||||
|
);
|
||||||
|
draw_rounded(
|
||||||
|
fb,
|
||||||
|
panel_x + edge,
|
||||||
|
panel_y + edge,
|
||||||
|
panel_width.saturating_sub(edge * 2),
|
||||||
|
panel_height.saturating_sub(edge * 2),
|
||||||
|
layout.radius.saturating_sub(edge),
|
||||||
|
PANEL,
|
||||||
|
);
|
||||||
|
|
||||||
for (slot, (index, entry)) in scene
|
for (slot, (index, entry)) in scene
|
||||||
.entries
|
.entries
|
||||||
@@ -557,40 +712,75 @@ fn render(
|
|||||||
.take(layout.visible)
|
.take(layout.visible)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let selected = index == scene.selected;
|
|
||||||
let row = layout.row(slot);
|
let row = layout.row(slot);
|
||||||
let (pill, text) = if selected {
|
|
||||||
(PILL_SELECTED, TEXT_SELECTED)
|
|
||||||
} else {
|
|
||||||
(PILL, TEXT)
|
|
||||||
};
|
|
||||||
draw_pill(fb, layout.x, row, layout.width, layout.height, pill);
|
|
||||||
|
|
||||||
let text_row = row + (layout.height - layout.cell) / 2;
|
let text_row = row + (layout.height - layout.cell) / 2;
|
||||||
if selected {
|
// Only the selected entry has a pill, which keeps the list calm.
|
||||||
|
let text = if index == scene.selected {
|
||||||
|
draw_pill(
|
||||||
|
fb,
|
||||||
|
layout.x,
|
||||||
|
row,
|
||||||
|
layout.width,
|
||||||
|
layout.height,
|
||||||
|
PILL_SELECTED,
|
||||||
|
);
|
||||||
draw_glyph(
|
draw_glyph(
|
||||||
fb,
|
fb,
|
||||||
layout.x + layout.cell,
|
layout.x + layout.cell,
|
||||||
text_row,
|
text_row,
|
||||||
layout.scale,
|
layout.scale,
|
||||||
&HEART,
|
&HEART,
|
||||||
text,
|
TEXT_SELECTED,
|
||||||
);
|
);
|
||||||
}
|
TEXT_SELECTED
|
||||||
|
} else {
|
||||||
|
TEXT
|
||||||
|
};
|
||||||
draw_text(
|
draw_text(
|
||||||
fb,
|
fb,
|
||||||
layout.x + layout.cell * 5 / 2 + layout.cell / 2,
|
layout.text_x,
|
||||||
text_row,
|
text_row,
|
||||||
layout.scale,
|
layout.scale,
|
||||||
(layout.width / layout.cell).saturating_sub(4),
|
layout.columns,
|
||||||
entry.title(),
|
entry.title(),
|
||||||
text,
|
text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_row = fb.height().saturating_sub(layout.cell * 2);
|
if layout.scrolls {
|
||||||
draw_centered(fb, status_row, layout.scale, &scene.status, TEXT_MUTED);
|
draw_scrollbar(fb, layout, offset, scene.entries.len());
|
||||||
draw_logo(fb, layout, scene.ticks);
|
}
|
||||||
|
|
||||||
|
// The status line is below a line across the panel, with the position when it scrolls.
|
||||||
|
let inner = panel_width.saturating_sub(layout.cell * 3);
|
||||||
|
let inner_x = panel_x + layout.cell * 3 / 2;
|
||||||
|
fb.fill_rect(inner_x, layout.footer, inner, layout.scale, PANEL_EDGE);
|
||||||
|
let status_row = layout.footer + layout.scale + layout.cell / 2;
|
||||||
|
// The position is at the right edge, so the status keeps clear of it on both sides.
|
||||||
|
let keep_clear = if layout.scrolls { layout.cell * 8 } else { 0 };
|
||||||
|
draw_centered(
|
||||||
|
fb,
|
||||||
|
inner_x + keep_clear,
|
||||||
|
inner.saturating_sub(keep_clear * 2),
|
||||||
|
status_row,
|
||||||
|
layout.scale,
|
||||||
|
&scene.status,
|
||||||
|
TEXT_MUTED,
|
||||||
|
);
|
||||||
|
if layout.scrolls {
|
||||||
|
let position = format!("{}/{}", scene.selected + 1, scene.entries.len());
|
||||||
|
let width = position.len() * layout.cell;
|
||||||
|
draw_text(
|
||||||
|
fb,
|
||||||
|
(inner_x + inner).saturating_sub(width),
|
||||||
|
status_row,
|
||||||
|
layout.scale,
|
||||||
|
position.len(),
|
||||||
|
&position,
|
||||||
|
TEXT_MUTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(position) = cursor {
|
if let Some(position) = cursor {
|
||||||
draw_cursor(fb, layout.scale, position);
|
draw_cursor(fb, layout.scale, position);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user