use crate::core::{Color, Font, HorizontalAlignment, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::widget::capability::coercion::expect_string;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyTipCorner {
#[default]
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl KeyTipCorner {
pub fn as_str(self) -> &'static str {
match self {
KeyTipCorner::TopLeft => "top_left",
KeyTipCorner::TopRight => "top_right",
KeyTipCorner::BottomLeft => "bottom_left",
KeyTipCorner::BottomRight => "bottom_right",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(token: &str) -> Option<Self> {
match token.trim().to_ascii_lowercase().as_str() {
"top_left" | "topleft" => Some(KeyTipCorner::TopLeft),
"top_right" | "topright" => Some(KeyTipCorner::TopRight),
"bottom_left" | "bottomleft" => Some(KeyTipCorner::BottomLeft),
"bottom_right" | "bottomright" => Some(KeyTipCorner::BottomRight),
_ => None,
}
}
}
pub struct KeyTipBadge {
base: BaseWidget,
tip_text: String,
badge_color: Option<Color>,
ink: Option<Color>,
corner: KeyTipCorner,
size: Size,
}
impl KeyTipBadge {
pub fn new(tip_text: impl Into<String>) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Label, Rect::new(0, 0, 18, 14), "KeyTipBadge"),
tip_text: tip_text.into(),
badge_color: None,
ink: None,
corner: KeyTipCorner::TopLeft,
size: Size::new(18, 14),
}
}
pub fn tip_text(&self) -> &str {
&self.tip_text
}
pub fn set_tip_text(&mut self, text: impl Into<String>) {
let text = text.into();
if self.tip_text == text {
return;
}
self.tip_text = text;
self.base.request_redraw();
}
pub fn size_hint(&self) -> Size {
self.size
}
pub fn set_size(&mut self, size: Size) {
if self.size == size {
return;
}
self.size = size;
let rect = self.geometry();
self.base.set_geometry(Rect::new(rect.x, rect.y, size.width, size.height));
self.base.request_redraw();
}
pub fn badge_color(&self) -> Option<Color> {
self.badge_color
}
pub fn set_badge_color(&mut self, color: Option<Color>) {
if self.badge_color == color {
return;
}
self.badge_color = color;
self.base.request_redraw();
}
pub fn ink(&self) -> Option<Color> {
self.ink
}
pub fn set_ink(&mut self, color: Option<Color>) {
if self.ink == color {
return;
}
self.ink = color;
self.base.request_redraw();
}
pub fn corner(&self) -> KeyTipCorner {
self.corner
}
pub fn set_corner(&mut self, corner: KeyTipCorner) {
if self.corner == corner {
return;
}
self.corner = corner;
self.base.request_redraw();
}
pub fn rect_for(&self, target: Rect) -> Rect {
let (x, y) = match self.corner {
KeyTipCorner::TopLeft => (target.x + 2, target.y + 2),
KeyTipCorner::TopRight => {
(target.x + target.width as i32 - self.size.width as i32 - 2, target.y + 2)
}
KeyTipCorner::BottomLeft => {
(target.x + 2, target.y + target.height as i32 - self.size.height as i32 - 2)
}
KeyTipCorner::BottomRight => (
target.x + target.width as i32 - self.size.width as i32 - 2,
target.y + target.height as i32 - self.size.height as i32 - 2,
),
};
Rect::new(x, y, self.size.width, self.size.height)
}
pub fn paint(&self, context: &mut RenderContext, rect: Rect) {
let (chip, ink) = self.resolve_colors();
context.fill_rect(rect, chip);
if self.tip_text.is_empty() {
return;
}
let font = Font::bold("Arial", (rect.height as f32 * 0.8).clamp(7.0, 12.0));
context.draw_text_fitted_cow(rect, &self.tip_text, &font, ink, HorizontalAlignment::Center);
}
fn resolve_colors(&self) -> (Color, Color) {
let chip = self
.badge_color
.or_else(|| crate::style::palette_shared().map(|colors| colors.primary))
.unwrap_or(Color::PRIMARY);
let ink = self.ink.unwrap_or_else(|| chip.contrast_color());
(chip, ink)
}
}
impl Widget for KeyTipBadge {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn size_hint(&self) -> Size {
self.size
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl Draw for KeyTipBadge {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
self.paint(context, rect);
}
}
impl EventHandler for KeyTipBadge {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
impl WidgetProperties for KeyTipBadge {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"tip_text" => Ok(CapabilityValue::String(self.tip_text.clone())),
"badge_color" => Ok(match self.badge_color {
Some(color) => CapabilityValue::Color(color),
None => CapabilityValue::Null,
}),
"ink" => Ok(match self.ink {
Some(color) => CapabilityValue::Color(color),
None => CapabilityValue::Null,
}),
"corner" => Ok(CapabilityValue::String(self.corner.as_str().to_string())),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"tip_text" => {
self.set_tip_text(expect_string(value)?);
Ok(())
}
"badge_color" => {
match value {
CapabilityValue::Null => self.set_badge_color(None),
CapabilityValue::Color(color) => self.set_badge_color(Some(color)),
_ => return Err(CapabilityAccessError::TypeMismatch),
}
Ok(())
}
"ink" => {
match value {
CapabilityValue::Null => self.set_ink(None),
CapabilityValue::Color(color) => self.set_ink(Some(color)),
_ => return Err(CapabilityAccessError::TypeMismatch),
}
Ok(())
}
"corner" => {
let token = expect_string(value)?;
let corner =
KeyTipCorner::from_str(&token).ok_or(CapabilityAccessError::TypeMismatch)?;
self.set_corner(corner);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["tip_text", "badge_color", "ink", "corner", BASE_PROPERTY_NAMES]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
use crate::widget::svg::render_to_svg;
#[test]
fn badge_defaults_to_the_ribbon_chip() {
let badge = KeyTipBadge::new("F");
assert_eq!(badge.tip_text(), "F");
assert_eq!(badge.size_hint(), Size::new(18, 14));
assert_eq!(badge.corner(), KeyTipCorner::TopLeft);
assert_eq!(badge.badge_color(), None);
}
#[test]
fn setting_the_tip_changes_the_painted_document() {
let mut badge = KeyTipBadge::new("F");
badge.set_geometry(Rect::new(0, 0, 18, 14));
let before = render_to_svg(&mut badge);
badge.set_tip_text("H1");
let after = render_to_svg(&mut badge);
assert_ne!(before, after, "the tip text is what the badge paints");
}
#[test]
fn a_corner_positions_the_chip_inside_its_target() {
let mut badge = KeyTipBadge::new("A");
badge.set_corner(KeyTipCorner::BottomRight);
let target = Rect::new(10, 20, 100, 30);
let rect = badge.rect_for(target);
assert_eq!(rect.right(), target.right() - 2);
assert_eq!(rect.bottom(), target.bottom() - 2);
assert!(target.contains_point(Point::new(rect.x, rect.y)));
}
#[test]
fn corner_tokens_round_trip() {
for corner in [
KeyTipCorner::TopLeft,
KeyTipCorner::TopRight,
KeyTipCorner::BottomLeft,
KeyTipCorner::BottomRight,
] {
assert_eq!(KeyTipCorner::from_str(corner.as_str()), Some(corner));
}
assert_eq!(KeyTipCorner::from_str("nowhere"), None);
}
}