use rlvgl_core::draw::{draw_widget_bg, fill_rounded_rect};
use rlvgl_core::event::Event;
use rlvgl_core::renderer::Renderer;
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarMode {
Normal,
Symmetrical,
Range,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarOrientation {
Auto,
Horizontal,
Vertical,
}
pub struct Bar {
bounds: Rect,
min: i32,
max: i32,
value: i32,
start_value: i32,
mode: BarMode,
orientation: BarOrientation,
pub style: Style,
pub indicator_color: Color,
}
impl Bar {
pub fn new(bounds: Rect, min: i32, max: i32) -> Self {
Self {
bounds,
min,
max,
value: min,
start_value: min,
mode: BarMode::Normal,
orientation: BarOrientation::Auto,
style: Style::default(),
indicator_color: Color(0, 0, 0, 255),
}
}
pub fn value(&self) -> i32 {
self.value
}
pub fn set_value(&mut self, value: i32) {
self.value = self.clamp_to_range(value);
}
pub fn start_value(&self) -> i32 {
if self.mode == BarMode::Range {
self.start_value
} else {
self.min
}
}
pub fn set_start_value(&mut self, value: i32) {
self.start_value = self.clamp_to_range(value);
}
pub fn set_range(&mut self, min: i32, max: i32) {
self.min = min;
self.max = max;
self.value = self.clamp_to_range(self.value);
self.start_value = self.clamp_to_range(self.start_value);
}
pub fn min_value(&self) -> i32 {
self.min
}
pub fn max_value(&self) -> i32 {
self.max
}
pub fn set_mode(&mut self, mode: BarMode) {
self.mode = mode;
}
pub fn mode(&self) -> BarMode {
self.mode
}
pub fn set_orientation(&mut self, orientation: BarOrientation) {
self.orientation = orientation;
}
pub fn orientation(&self) -> BarOrientation {
self.orientation
}
fn resolved_orientation(&self) -> BarOrientation {
match self.orientation {
BarOrientation::Auto if self.bounds.width >= self.bounds.height => {
BarOrientation::Horizontal
}
BarOrientation::Auto => BarOrientation::Vertical,
orientation => orientation,
}
}
fn clamp_to_range(&self, value: i32) -> i32 {
if self.min <= self.max {
value.clamp(self.min, self.max)
} else {
value.clamp(self.max, self.min)
}
}
fn offset_for_value(&self, value: i32, length: i32) -> i32 {
if length <= 0 || self.min == self.max {
return 0;
}
let den = i64::from(self.max) - i64::from(self.min);
let den_abs = den.unsigned_abs() as i64;
let num = if den > 0 {
i64::from(value) - i64::from(self.min)
} else {
i64::from(self.min) - i64::from(value)
}
.clamp(0, den_abs);
((num * i64::from(length)) / den_abs) as i32
}
fn indicator_offsets(&self, length: i32) -> Option<(i32, i32)> {
if self.min == self.max {
return None;
}
let value = self.offset_for_value(self.value, length);
let (start, end) = match self.mode {
BarMode::Normal => (0, value),
BarMode::Range => (self.offset_for_value(self.start_value, length), value),
BarMode::Symmetrical if self.range_crosses_zero() => {
(self.offset_for_value(0, length), value)
}
BarMode::Symmetrical => (0, value),
};
(start != end).then_some((start.min(end), start.max(end)))
}
fn range_crosses_zero(&self) -> bool {
(self.min <= 0 && self.max >= 0) || (self.max <= 0 && self.min >= 0)
}
fn indicator_rect(&self) -> Option<Rect> {
if self.bounds.width <= 0 || self.bounds.height <= 0 {
return None;
}
match self.resolved_orientation() {
BarOrientation::Horizontal | BarOrientation::Auto => {
let (start, end) = self.indicator_offsets(self.bounds.width)?;
Some(Rect {
x: self.bounds.x + start,
y: self.bounds.y,
width: end - start,
height: self.bounds.height,
})
}
BarOrientation::Vertical => {
let (start, end) = self.indicator_offsets(self.bounds.height)?;
Some(Rect {
x: self.bounds.x,
y: self.bounds.y + self.bounds.height - end,
width: self.bounds.width,
height: end - start,
})
}
}
}
}
impl Widget for Bar {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
let color = self.indicator_color.with_alpha(self.style.alpha);
if color.3 == 0 {
return;
}
if let Some(rect) = self.indicator_rect() {
fill_rounded_rect(renderer, rect, color, self.style.radius);
}
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_and_start_values_clamp_for_reversed_ranges() {
let mut bar = Bar::new(rect(0, 0, 100, 10), 100, 0);
bar.set_value(150);
assert_eq!(bar.value(), 100);
bar.set_value(-10);
assert_eq!(bar.value(), 0);
bar.set_start_value(75);
assert_eq!(bar.start_value(), 100);
bar.set_mode(BarMode::Range);
assert_eq!(bar.start_value(), 75);
}
#[test]
fn horizontal_normal_indicator_uses_value_fraction() {
let mut bar = Bar::new(rect(10, 20, 100, 8), 0, 100);
bar.set_value(40);
assert_eq!(bar.indicator_rect(), Some(rect(10, 20, 40, 8)));
}
#[test]
fn vertical_range_indicator_uses_bottom_origin() {
let mut bar = Bar::new(rect(0, 0, 8, 100), 0, 100);
bar.set_orientation(BarOrientation::Vertical);
bar.set_mode(BarMode::Range);
bar.set_start_value(25);
bar.set_value(75);
assert_eq!(bar.indicator_rect(), Some(rect(0, 25, 8, 50)));
}
#[test]
fn symmetrical_indicator_uses_zero_baseline_when_inside_range() {
let mut bar = Bar::new(rect(0, 0, 100, 8), -100, 100);
bar.set_mode(BarMode::Symmetrical);
bar.set_value(-50);
assert_eq!(bar.indicator_rect(), Some(rect(25, 0, 25, 8)));
}
#[test]
fn set_bounds_adopts_layout_bounds() {
let mut bar = Bar::new(rect(0, 0, 10, 2), 0, 10);
bar.set_bounds(rect(3, 4, 20, 5));
assert_eq!(bar.bounds(), rect(3, 4, 20, 5));
}
fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
}