use crate::compat::ToString;
use crate::core::{Color, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::widget::capability::coercion::{expect_string, expect_u32};
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::display_widgets::{draw_line, draw_shaded_line};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineOrientation {
Horizontal,
Vertical,
}
pub use crate::widget::base_widgets::frame::FrameShadow as LineStyle;
pub struct Line {
base: BaseWidget,
orientation: LineOrientation,
thickness: u32,
color: Option<Color>,
style: LineStyle,
}
impl Line {
pub fn new(orientation: LineOrientation, rect: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Line, rect, "Line"),
orientation,
thickness: 2,
color: None,
style: LineStyle::Plain,
}
}
pub fn orientation(&self) -> LineOrientation {
self.orientation
}
pub fn set_orientation(&mut self, ori: LineOrientation) {
if self.orientation == ori {
return;
}
self.orientation = ori;
self.base.request_redraw();
}
pub fn set_thickness(&mut self, t: u32) {
let thickness = t.max(1);
if self.thickness == thickness {
return;
}
self.thickness = thickness;
self.base.request_redraw();
}
pub fn thickness(&self) -> u32 {
self.thickness
}
pub fn color(&self) -> Option<Color> {
self.color
}
pub fn set_color(&mut self, color: Option<Color>) {
if self.color == color {
return;
}
self.color = color;
self.base.request_redraw();
}
pub fn line_style(&self) -> LineStyle {
self.style
}
pub fn set_line_style(&mut self, style: LineStyle) {
if self.style == style {
return;
}
self.style = style;
self.base.request_redraw();
}
fn line_color(&self) -> Color {
self.color.or_else(|| Widget::style(self).border_color).unwrap_or(Color::rgb(180, 180, 180))
}
fn draw_shaded(&self, context: &mut RenderContext, rect: Rect, vertical: bool, sunken: bool) {
draw_shaded_line(context, rect, vertical, self.line_color(), sunken);
}
}
impl Widget for Line {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
match self.orientation {
LineOrientation::Horizontal => Size::new(120, self.thickness.max(2)),
LineOrientation::Vertical => Size::new(self.thickness.max(2), 120),
}
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Line {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"orientation" => {
let orientation = match self.orientation() {
LineOrientation::Horizontal => "horizontal",
LineOrientation::Vertical => "vertical",
};
Ok(CapabilityValue::String(orientation.to_string()))
}
"color" => Ok(CapabilityValue::Color(self.line_color())),
"thickness" => Ok(CapabilityValue::UInt(u64::from(self.thickness()))),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"orientation" => {
let token = expect_string(value)?;
let orientation = match token.as_str() {
"horizontal" => LineOrientation::Horizontal,
"vertical" => LineOrientation::Vertical,
_ => return Err(CapabilityAccessError::TypeMismatch),
};
self.set_orientation(orientation);
Ok(())
}
"color" => {
match value {
CapabilityValue::Null => self.set_color(None),
CapabilityValue::Color(color) => self.set_color(Some(color)),
CapabilityValue::String(raw) => match Color::parse_hex(&raw) {
Some(color) => self.set_color(Some(color)),
None => return Err(CapabilityAccessError::TypeMismatch),
},
_ => return Err(CapabilityAccessError::TypeMismatch),
}
Ok(())
}
"thickness" => {
self.set_thickness(expect_u32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["orientation", "thickness", "color", BASE_PROPERTY_NAMES]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"set_orientation" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl EventHandler for Line {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
impl Draw for Line {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let is_vertical = self.orientation == LineOrientation::Vertical;
let line_color = self.line_color();
match self.style {
LineStyle::Plain => {
draw_line(context, rect, is_vertical, self.thickness, line_color);
}
LineStyle::Sunken => self.draw_shaded(context, rect, is_vertical, true),
LineStyle::Raised => self.draw_shaded(context, rect, is_vertical, false),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Color, Rect, Size};
use crate::render::{PaintBackend, SoftwarePaintBackend};
#[test]
fn line_creation() {
let line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
assert_eq!(line.orientation(), LineOrientation::Horizontal);
assert_eq!(line.kind(), WidgetKind::Line);
}
#[test]
fn line_colour_override_round_trips() {
use crate::widget::capability::WidgetProperties;
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
assert_eq!(line.color(), None);
line.set_color(Some(Color::RED));
assert_eq!(line.color(), Some(Color::RED));
assert_eq!(
line.get("color"),
Ok(CapabilityValue::Color(Color::RED)),
"the published colour is the effective one the painter uses"
);
line.set_color(None);
assert_eq!(line.color(), None, "`None` restores theme-following");
}
#[test]
fn line_orientation() {
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
assert_eq!(line.orientation(), LineOrientation::Horizontal);
line.set_orientation(LineOrientation::Vertical);
assert_eq!(line.orientation(), LineOrientation::Vertical);
line.set_orientation(LineOrientation::Horizontal);
assert_eq!(line.orientation(), LineOrientation::Horizontal);
}
#[test]
fn line_set_thickness() {
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
line.set_thickness(5);
assert_eq!(line.thickness, 5);
line.set_thickness(0);
assert_eq!(line.thickness, 1);
}
#[test]
fn line_draw_no_panic() {
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
let mut backend = SoftwarePaintBackend::new(Size::new(200, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut context = RenderContext::new(&mut backend);
line.draw(&mut context);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn line_draw_vertical_no_panic() {
let mut line = Line::new(LineOrientation::Vertical, Rect::new(0, 0, 4, 200));
let mut backend = SoftwarePaintBackend::new(Size::new(10, 200), 1.0);
backend.begin_frame(Color::WHITE);
let mut context = RenderContext::new(&mut backend);
line.draw(&mut context);
backend.end_frame();
let rgba = backend.frame_rgba();
assert!(!rgba.is_empty());
}
#[test]
fn line_draw_zero_geometry_no_panic() {
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 0, 0));
let mut backend = SoftwarePaintBackend::new(Size::new(10, 10), 1.0);
backend.begin_frame(Color::WHITE);
let mut context = RenderContext::new(&mut backend);
line.draw(&mut context);
backend.end_frame();
}
#[test]
fn line_size_hint() {
let line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
assert_eq!(line.size_hint(), Size::new(120, 2));
let mut line = Line::new(LineOrientation::Vertical, Rect::new(0, 0, 4, 200));
line.set_thickness(6);
assert_eq!(line.size_hint(), Size::new(6, 120));
}
#[test]
fn line_geometry_delegation() {
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
line.set_geometry(Rect::new(5, 5, 150, 6));
assert_eq!(line.geometry(), Rect::new(5, 5, 150, 6));
}
#[test]
fn line_event_delegation() {
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
line.handle_event(&Event::KeyDown((37, 0)));
}
#[test]
fn line_style_tokens_round_trip() {
assert_eq!(LineStyle::default(), LineStyle::Plain);
assert_eq!(LineStyle::from_token("plain"), Some(LineStyle::Plain));
assert_eq!(LineStyle::from_token("sunken"), Some(LineStyle::Sunken));
assert_eq!(LineStyle::from_token("raised"), Some(LineStyle::Raised));
assert_eq!(LineStyle::from_token("chartreuse"), None);
assert_eq!(LineStyle::Sunken.token(), "sunken");
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 4));
assert_eq!(line.line_style(), LineStyle::Plain);
line.set_line_style(LineStyle::Raised);
assert_eq!(line.line_style(), LineStyle::Raised);
line.set_line_style(LineStyle::Sunken);
assert_eq!(line.line_style(), LineStyle::Sunken);
}
#[test]
fn sunken_and_raised_paint_a_double_line() {
let _theme_guard = crate::style::theme_test_guard();
let mut line = Line::new(LineOrientation::Horizontal, Rect::new(0, 0, 200, 6));
let plain = crate::widget::svg::render_to_svg(&mut line);
assert_eq!(plain.matches("<line").count(), 1, "Plain paints one line: {plain}");
line.set_line_style(LineStyle::Sunken);
let sunken = crate::widget::svg::render_to_svg(&mut line);
assert_eq!(sunken.matches("<line").count(), 2, "Sunken paints two lines: {sunken}");
line.set_line_style(LineStyle::Raised);
let raised = crate::widget::svg::render_to_svg(&mut line);
assert_eq!(raised.matches("<line").count(), 2, "Raised paints two lines: {raised}");
assert_ne!(sunken, raised, "the two styles swap the highlight and shadow edges");
}
#[test]
fn a_vertical_shaded_line_stays_inside_its_rectangle() {
let _theme_guard = crate::style::theme_test_guard();
let mut line = Line::new(LineOrientation::Vertical, Rect::new(0, 0, 6, 120));
line.set_line_style(LineStyle::Sunken);
let svg = crate::widget::svg::render_to_svg(&mut line);
assert_eq!(svg.matches("<line").count(), 2, "two edges: {svg}");
for x in [2, 3] {
assert!(
svg.contains(&format!("x1=\"{x}\"")) && svg.contains(&format!("x2=\"{x}\"")),
"the two edges are adjacent columns at x={x}: {svg}"
);
}
}
}