use std::sync::Arc;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::geometry::{Rect, Size};
use crate::text::Font;
use crate::widget::{paint_subtree, Widget};
use crate::widgets::label::{Label, LabelAlign};
use super::super::geometry::PopupLayout;
use super::super::model::MenuEntry;
use super::super::paint::popup_row_text_color;
pub struct BarLabels {
labels: Vec<Label>,
}
impl BarLabels {
pub fn new() -> Self {
Self { labels: Vec::new() }
}
pub fn sync_to(&mut self, font: &Arc<Font>, font_size: f64, labels: &[&str]) {
if self.labels.len() != labels.len() {
self.labels = labels
.iter()
.map(|text| make_bar_label(text, font, font_size))
.collect();
return;
}
for (slot, text) in self.labels.iter_mut().zip(labels.iter()) {
if slot.text_str() != *text {
slot.set_text(*text);
}
}
}
pub fn paint_in(&mut self, ctx: &mut dyn DrawCtx, idx: usize, slot: Rect, color: Color) {
let Some(label) = self.labels.get_mut(idx) else {
return;
};
label.set_color(color);
let avail_h = slot.height.max(1.0);
let size = label.layout(Size::new(slot.width, avail_h));
let x = slot.x + 9.0;
let y = slot.y + (slot.height - size.height) * 0.5;
label.set_bounds(Rect::new(0.0, 0.0, size.width, size.height));
ctx.save();
ctx.translate(x, y);
paint_subtree(label, ctx);
ctx.restore();
}
}
impl Default for BarLabels {
fn default() -> Self {
Self::new()
}
}
pub struct PopupRowLabels {
pub label: Label,
pub shortcut: Option<Label>,
}
pub struct PopupLabels {
rows: Vec<Vec<Option<PopupRowLabels>>>,
last_font_ptr: *const Font,
last_font_size: f64,
}
impl PopupLabels {
pub fn new() -> Self {
Self {
rows: Vec::new(),
last_font_ptr: std::ptr::null(),
last_font_size: 0.0,
}
}
pub fn sync_to(
&mut self,
font: &Arc<Font>,
font_size: f64,
items: &[MenuEntry],
layouts: &[PopupLayout],
) {
let font_ptr = Arc::as_ptr(font);
let font_changed =
font_ptr != self.last_font_ptr || (self.last_font_size - font_size).abs() > 0.01;
if font_changed {
self.rows.clear();
self.last_font_ptr = font_ptr;
self.last_font_size = font_size;
}
if self.rows.len() != layouts.len() {
self.rows.resize_with(layouts.len(), Vec::new);
}
for (level_idx, layout) in layouts.iter().enumerate() {
let level_items = items_for_layout(items, &layout.path_prefix);
let level = &mut self.rows[level_idx];
if level.len() != layout.rows.len() {
level.clear();
level.resize_with(layout.rows.len(), || None);
}
for (row_idx, row) in layout.rows.iter().enumerate() {
let Some(item_idx) = row.item_index else {
level[row_idx] = None;
continue;
};
let Some(MenuEntry::Item(item)) = level_items.get(item_idx) else {
level[row_idx] = None;
continue;
};
match &mut level[row_idx] {
Some(existing) => {
if existing.label.text_str() != item.label {
existing.label.set_text(&item.label);
}
match (&mut existing.shortcut, item.shortcut.as_deref()) {
(Some(slot), Some(text)) => {
if slot.text_str() != text {
slot.set_text(text);
}
}
(slot @ Some(_), None) => *slot = None,
(slot @ None, Some(text)) => {
*slot = Some(make_shortcut_label(text, font, font_size));
}
(None, None) => {}
}
}
slot @ None => {
*slot = Some(PopupRowLabels {
label: make_label(&item.label, font, font_size),
shortcut: item
.shortcut
.as_deref()
.map(|s| make_shortcut_label(s, font, font_size)),
});
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn paint_row(
&mut self,
ctx: &mut dyn DrawCtx,
level_idx: usize,
row_idx: usize,
row_rect: Rect,
label_x: f64,
shortcut_right_pad: f64,
color: Color,
) {
let Some(level) = self.rows.get_mut(level_idx) else {
return;
};
let Some(Some(row)) = level.get_mut(row_idx) else {
return;
};
row.label.set_color(color);
let inner_w = (row_rect.width - label_x).max(0.0);
let size = row.label.layout(Size::new(inner_w, row_rect.height));
row.label
.set_bounds(Rect::new(0.0, 0.0, size.width, size.height));
let lx = row_rect.x + label_x;
let ly = row_rect.y + (row_rect.height - size.height) * 0.5;
ctx.save();
ctx.translate(lx, ly);
paint_subtree(&mut row.label, ctx);
ctx.restore();
if let Some(shortcut) = row.shortcut.as_mut() {
shortcut.set_color(color);
let s_size = shortcut.layout(Size::new(row_rect.width, row_rect.height));
shortcut.set_bounds(Rect::new(0.0, 0.0, s_size.width, s_size.height));
let sx = row_rect.x + row_rect.width - shortcut_right_pad - s_size.width;
let sy = row_rect.y + (row_rect.height - s_size.height) * 0.5;
ctx.save();
ctx.translate(sx, sy);
paint_subtree(shortcut, ctx);
ctx.restore();
}
}
#[allow(clippy::too_many_arguments)]
pub fn paint_row_with_state(
&mut self,
ctx: &mut dyn DrawCtx,
level_idx: usize,
row_idx: usize,
row_rect: Rect,
label_x: f64,
shortcut_right_pad: f64,
enabled: bool,
open: bool,
) {
let color = popup_row_text_color(ctx, enabled, open);
self.paint_row(
ctx,
level_idx,
row_idx,
row_rect,
label_x,
shortcut_right_pad,
color,
);
}
}
impl Default for PopupLabels {
fn default() -> Self {
Self::new()
}
}
impl Clone for PopupLabels {
fn clone(&self) -> Self {
Self::new()
}
}
fn make_label(text: &str, font: &Arc<Font>, font_size: f64) -> Label {
Label::new(text, Arc::clone(font))
.with_font_size(font_size)
.with_align(LabelAlign::Left)
}
fn make_bar_label(text: &str, font: &Arc<Font>, font_size: f64) -> Label {
make_label(text, font, font_size).with_has_backbuffer(false)
}
fn make_shortcut_label(text: &str, font: &Arc<Font>, font_size: f64) -> Label {
Label::new(text, Arc::clone(font))
.with_font_size(font_size)
.with_align(LabelAlign::Left)
}
fn items_for_layout<'a>(items: &'a [MenuEntry], path: &[usize]) -> &'a [MenuEntry] {
let mut current = items;
for &idx in path {
let Some(MenuEntry::Item(item)) = current.get(idx) else {
return current;
};
current = &item.submenu;
}
current
}
#[cfg(test)]
mod lcd_tests {
use super::BarLabels;
use crate::color::Color;
use crate::draw_ctx::DrawCtx;
use crate::font_settings::{clear_lcd_enabled_override, set_lcd_enabled};
use crate::geometry::Rect;
use crate::lcd_coverage::LcdBuffer;
use crate::lcd_gfx_ctx::LcdGfxCtx;
use crate::text::Font;
use std::sync::Arc;
const FONT_BYTES: &[u8] = include_bytes!("../../../../../demo/assets/CascadiaCode.ttf");
fn font() -> Arc<Font> {
Arc::new(Font::from_slice(FONT_BYTES).expect("font"))
}
fn has_subpixel(buf: &LcdBuffer) -> bool {
buf.color_plane()
.chunks_exact(3)
.any(|p| p[0] != p[1] || p[1] != p[2])
}
#[test]
fn bar_label_keeps_lcd_subpixel_inside_backbuffer_ctx() {
set_lcd_enabled(true);
let font = font();
let mut bar = BarLabels::new();
bar.sync_to(&font, 14.0, &["File"]);
let mut buf = LcdBuffer::new(120, 28);
{
let mut ctx = LcdGfxCtx::new(&mut buf);
ctx.clear(Color::white());
bar.paint_in(&mut ctx, 0, Rect::new(0.0, 0.0, 80.0, 28.0), Color::black());
}
let subpixel = has_subpixel(&buf);
clear_lcd_enabled_override();
assert!(
subpixel,
"menu-bar label lost LCD subpixel coverage when painted into the \
bar's LcdCoverage backbuffer (nested-backbuffer collapse regression)"
);
}
}