You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
9.1 KiB
308 lines
9.1 KiB
use crate::{
|
|
strutil::ShortString,
|
|
ui::{
|
|
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
|
|
geometry::{Alignment2D, Offset, Rect},
|
|
shape,
|
|
shape::Renderer,
|
|
},
|
|
};
|
|
|
|
use heapless::String;
|
|
|
|
use super::super::{theme, ButtonDetails, ButtonLayout, Choice};
|
|
|
|
const ICON_RIGHT_PADDING: i16 = 2;
|
|
|
|
/// Simple string component used as a choice item.
|
|
#[derive(Clone)]
|
|
pub struct ChoiceItem {
|
|
text: ShortString,
|
|
icon: Option<Icon>,
|
|
btn_layout: ButtonLayout,
|
|
font: Font,
|
|
middle_action_without_release: bool,
|
|
}
|
|
|
|
impl ChoiceItem {
|
|
pub fn new<U: AsRef<str>>(text: U, btn_layout: ButtonLayout) -> Self {
|
|
Self {
|
|
text: String::from(text.as_ref()),
|
|
icon: None,
|
|
btn_layout,
|
|
font: theme::FONT_CHOICE_ITEMS,
|
|
middle_action_without_release: false,
|
|
}
|
|
}
|
|
|
|
/// Allows to add the icon.
|
|
pub fn with_icon(mut self, icon: Icon) -> Self {
|
|
self.icon = Some(icon);
|
|
self
|
|
}
|
|
|
|
/// Allows to change the font.
|
|
pub fn with_font(mut self, font: Font) -> Self {
|
|
self.font = font;
|
|
self
|
|
}
|
|
|
|
/// Allows for middle action without release.
|
|
pub fn with_middle_action_without_release(mut self) -> Self {
|
|
self.middle_action_without_release = true;
|
|
if let Some(middle) = self.btn_layout.btn_middle.as_mut() {
|
|
middle.send_long_press = true;
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Setting left button.
|
|
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails>) {
|
|
self.btn_layout.btn_left = btn_left;
|
|
}
|
|
|
|
/// Setting middle button.
|
|
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails>) {
|
|
self.btn_layout.btn_middle = btn_middle;
|
|
}
|
|
|
|
/// Setting right button.
|
|
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails>) {
|
|
self.btn_layout.btn_right = btn_right;
|
|
}
|
|
|
|
/// Changing the text.
|
|
pub fn set_text(&mut self, text: ShortString) {
|
|
self.text = text;
|
|
}
|
|
|
|
fn side_text(&self) -> Option<&str> {
|
|
if self.icon.is_some() {
|
|
None
|
|
} else {
|
|
Some(self.text.as_ref())
|
|
}
|
|
}
|
|
|
|
pub fn content(&self) -> &str {
|
|
self.text.as_ref()
|
|
}
|
|
}
|
|
|
|
impl Choice for ChoiceItem {
|
|
/// Painting the item as the main choice in the middle.
|
|
/// Showing both the icon and text, if the icon is available.
|
|
fn paint_center(&self, area: Rect, inverse: bool) {
|
|
let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font);
|
|
paint_rounded_highlight(
|
|
area,
|
|
Offset::new(width, self.font.visible_text_height("Ay")),
|
|
inverse,
|
|
);
|
|
paint_text_icon(
|
|
area,
|
|
width,
|
|
Some(self.text.as_ref()),
|
|
self.icon,
|
|
self.font,
|
|
inverse,
|
|
);
|
|
}
|
|
|
|
/// Painting the item as the main choice in the middle.
|
|
/// Showing both the icon and text, if the icon is available.
|
|
fn render_center<'s>(&self, target: &mut impl Renderer<'s>, area: Rect, inverse: bool) {
|
|
let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font);
|
|
render_rounded_highlight(
|
|
target,
|
|
area,
|
|
Offset::new(width, self.font.visible_text_height("Ay")),
|
|
inverse,
|
|
);
|
|
render_text_icon(
|
|
target,
|
|
area,
|
|
width,
|
|
Some(self.text.as_ref()),
|
|
self.icon,
|
|
self.font,
|
|
inverse,
|
|
);
|
|
}
|
|
|
|
/// Getting the overall width in pixels when displayed in center.
|
|
/// That means both the icon and text will be shown.
|
|
fn width_center(&self) -> i16 {
|
|
text_icon_width(Some(self.text.as_ref()), self.icon, self.font)
|
|
}
|
|
|
|
/// Getting the non-central width in pixels.
|
|
/// It will show an icon if defined, otherwise the text, not both.
|
|
fn width_side(&self) -> i16 {
|
|
text_icon_width(self.side_text(), self.icon, self.font)
|
|
}
|
|
|
|
/// Painting smaller version of the item on the side.
|
|
fn paint_side(&self, area: Rect) {
|
|
let width = text_icon_width(self.side_text(), self.icon, self.font);
|
|
paint_text_icon(area, width, self.side_text(), self.icon, self.font, false);
|
|
}
|
|
|
|
/// Painting smaller version of the item on the side.
|
|
fn render_side<'s>(&self, target: &mut impl Renderer<'s>, area: Rect) {
|
|
let width = text_icon_width(self.side_text(), self.icon, self.font);
|
|
render_text_icon(
|
|
target,
|
|
area,
|
|
width,
|
|
self.side_text(),
|
|
self.icon,
|
|
self.font,
|
|
false,
|
|
);
|
|
}
|
|
|
|
/// Getting current button layout.
|
|
fn btn_layout(&self) -> ButtonLayout {
|
|
self.btn_layout.clone()
|
|
}
|
|
|
|
/// Whether to do middle action without release
|
|
fn trigger_middle_without_release(&self) -> bool {
|
|
self.middle_action_without_release
|
|
}
|
|
}
|
|
|
|
fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {
|
|
let bound = theme::BUTTON_OUTLINE;
|
|
let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1);
|
|
let x_size = size.x + 2 * bound;
|
|
let y_size = size.y + 2 * bound;
|
|
let outline_size = Offset::new(x_size, y_size);
|
|
let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size);
|
|
if inverse {
|
|
rect_fill(outline, theme::FG);
|
|
rect_fill_corners(outline, theme::BG);
|
|
} else {
|
|
rect_outline_rounded(outline, theme::FG, theme::BG, 1);
|
|
}
|
|
}
|
|
|
|
fn render_rounded_highlight<'s>(
|
|
target: &mut impl Renderer<'s>,
|
|
area: Rect,
|
|
size: Offset,
|
|
inverse: bool,
|
|
) {
|
|
let bound = theme::BUTTON_OUTLINE;
|
|
let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1);
|
|
let x_size = size.x + 2 * bound;
|
|
let y_size = size.y + 2 * bound;
|
|
let outline_size = Offset::new(x_size, y_size);
|
|
let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size);
|
|
if inverse {
|
|
shape::Bar::new(outline)
|
|
.with_radius(1)
|
|
.with_bg(theme::FG)
|
|
.render(target);
|
|
} else {
|
|
// Draw outline by drawing two rounded rectangles
|
|
shape::Bar::new(outline)
|
|
.with_radius(1)
|
|
.with_bg(theme::FG)
|
|
.render(target);
|
|
|
|
shape::Bar::new(outline.shrink(1))
|
|
.with_radius(1)
|
|
.with_bg(theme::BG)
|
|
.render(target);
|
|
}
|
|
}
|
|
|
|
fn text_icon_width(text: Option<&str>, icon: Option<Icon>, font: Font) -> i16 {
|
|
match (text, icon) {
|
|
(Some(text), Some(icon)) => {
|
|
icon.toif.width() + ICON_RIGHT_PADDING + font.visible_text_width(text)
|
|
}
|
|
(Some(text), None) => font.visible_text_width(text),
|
|
(None, Some(icon)) => icon.toif.width(),
|
|
(None, None) => 0,
|
|
}
|
|
}
|
|
|
|
fn paint_text_icon(
|
|
area: Rect,
|
|
width: i16,
|
|
text: Option<&str>,
|
|
icon: Option<Icon>,
|
|
font: Font,
|
|
inverse: bool,
|
|
) {
|
|
let fg_color = if inverse { theme::BG } else { theme::FG };
|
|
let bg_color = if inverse { theme::FG } else { theme::BG };
|
|
|
|
let mut baseline = area.bottom_center() - Offset::x(width / 2);
|
|
if let Some(icon) = icon {
|
|
let height_diff = font.visible_text_height("Ay") - icon.toif.height();
|
|
let vertical_offset = Offset::y(-height_diff / 2);
|
|
icon.draw(
|
|
baseline + vertical_offset,
|
|
Alignment2D::BOTTOM_LEFT,
|
|
fg_color,
|
|
bg_color,
|
|
);
|
|
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
|
|
}
|
|
|
|
if let Some(text) = text {
|
|
// Possibly shifting the baseline left, when there is a text bearing.
|
|
// This is to center the text properly.
|
|
baseline = baseline - Offset::x(font.start_x_bearing(text));
|
|
display::text_left(baseline, text, font, fg_color, bg_color);
|
|
}
|
|
}
|
|
|
|
fn render_text_icon<'s>(
|
|
target: &mut impl Renderer<'s>,
|
|
area: Rect,
|
|
width: i16,
|
|
text: Option<&str>,
|
|
icon: Option<Icon>,
|
|
font: Font,
|
|
inverse: bool,
|
|
) {
|
|
let fg_color = if inverse { theme::BG } else { theme::FG };
|
|
|
|
let mut baseline = area.bottom_center() - Offset::x(width / 2);
|
|
if let Some(icon) = icon {
|
|
let height_diff = font.visible_text_height("Ay") - icon.toif.height();
|
|
let vertical_offset = Offset::y(-height_diff / 2);
|
|
shape::ToifImage::new(baseline + vertical_offset, icon.toif)
|
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
|
.with_fg(fg_color)
|
|
.render(target);
|
|
|
|
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
|
|
}
|
|
|
|
if let Some(text) = text {
|
|
// Possibly shifting the baseline left, when there is a text bearing.
|
|
// This is to center the text properly.
|
|
baseline = baseline - Offset::x(font.start_x_bearing(text));
|
|
shape::Text::new(baseline, text)
|
|
.with_font(font)
|
|
.with_fg(fg_color)
|
|
.render(target);
|
|
}
|
|
}
|
|
|
|
// DEBUG-ONLY SECTION BELOW
|
|
|
|
#[cfg(feature = "ui_debug")]
|
|
impl crate::trace::Trace for ChoiceItem {
|
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
|
t.component("ChoiceItem");
|
|
t.string("content", self.text.as_str().into());
|
|
}
|
|
}
|