use crate::event::{Event, EventCtx, Key, Modifiers};
use crate::geometry::{Color, Rect};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::Widget;
use crate::widgets::mnemonic::{ParsedLabel, draw_label_with_mnemonic, parse_label};
pub struct FocusLabel {
pub rect: Rect,
parsed: ParsedLabel,
size: Option<f32>,
color: Option<Color>,
background: Option<Color>,
}
impl FocusLabel {
pub fn new(rect: Rect, text: impl AsRef<str>) -> Self {
Self {
rect,
parsed: parse_label(text.as_ref()),
size: None,
color: None,
background: None,
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn with_size(mut self, size: f32) -> Self {
self.size = Some(size);
self
}
pub fn with_background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn mnemonic(&self) -> Option<char> {
self.parsed.mnemonic_char
}
fn is_accelerator(&self, ch: char, modifiers: Modifiers) -> bool {
modifiers.mnemonic_alt() && self.parsed.mnemonic_char == Some(ch.to_ascii_lowercase())
}
fn fire(&self, ctx: &mut EventCtx) {
ctx.request_focus_next();
ctx.swallow_key_until_release();
ctx.consume_event();
ctx.request_paint();
}
}
impl Widget for FocusLabel {
fn bounds(&self) -> Rect {
self.rect
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
if let Some(bg) = self.background {
painter.fill_rect(self.rect, bg);
}
let size = self.size.unwrap_or(theme.font_size);
let color = self.color.unwrap_or(theme.text);
let line_height = painter.measure_text("", size).h.max(1);
let y = self.rect.y + ((self.rect.h - line_height) / 2).max(0);
let saved = painter.push_clip(self.rect);
draw_label_with_mnemonic(painter, self.rect.x, y, 0, &self.parsed, size, color);
painter.restore_clip(saved);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if self.parsed.mnemonic_char.is_none() {
return;
}
match event {
Event::KeyDown {
key: Key::Char(ch),
modifiers,
}
| Event::Char { ch, modifiers }
if self.is_accelerator(*ch, *modifiers) =>
{
self.fire(ctx);
}
_ => {}
}
}
fn accepts_accelerators(&self) -> bool {
self.parsed.mnemonic_char.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Modifiers;
fn alt() -> Modifiers {
Modifiers {
alt: true,
..Modifiers::default()
}
}
fn altgr() -> Modifiers {
Modifiers {
alt: true,
alt_graph: true,
..Modifiers::default()
}
}
fn key(ch: char, modifiers: Modifiers) -> Event {
Event::KeyDown {
key: Key::Char(ch),
modifiers,
}
}
#[test]
fn matched_mnemonic_requests_focus_next() {
let mut label = FocusLabel::new(Rect::new(0, 0, 80, 20), "Last &name:");
let mut ctx = EventCtx::new();
label.event(&key('n', alt()), &mut ctx);
assert!(ctx.is_focus_next_requested());
assert!(ctx.is_consumed());
assert!(ctx.swallow_key);
}
#[test]
fn ignores_letter_without_alt() {
let mut label = FocusLabel::new(Rect::new(0, 0, 80, 20), "&Name:");
let mut ctx = EventCtx::new();
label.event(&key('n', Modifiers::default()), &mut ctx);
assert!(!ctx.is_focus_next_requested());
assert!(!ctx.is_consumed());
}
#[test]
fn ignores_altgr_so_composed_chars_pass_through() {
let mut label = FocusLabel::new(Rect::new(0, 0, 80, 20), "&Name:");
let mut ctx = EventCtx::new();
label.event(&key('n', altgr()), &mut ctx);
assert!(!ctx.is_focus_next_requested());
}
#[test]
fn match_is_case_insensitive() {
let mut label = FocusLabel::new(Rect::new(0, 0, 80, 20), "&Name:");
let mut ctx = EventCtx::new();
label.event(&key('N', alt()), &mut ctx);
assert!(ctx.is_focus_next_requested());
}
#[test]
fn label_without_mnemonic_is_inert() {
let label = FocusLabel::new(Rect::new(0, 0, 80, 20), "Plain caption");
assert_eq!(label.mnemonic(), None);
assert!(!label.accepts_accelerators());
}
#[test]
fn paint_path_does_not_panic() {
let backend = crate::mock::MockBackend::new(120, 24);
let mut marked =
FocusLabel::new(Rect::new(2, 2, 116, 20), "Last &name:").with_background(Color::WHITE);
backend.render(&mut marked);
let mut plain = FocusLabel::new(Rect::new(2, 2, 116, 20), "no marker");
crate::mock::MockBackend::new(120, 24).render(&mut plain);
}
}