use super::Brick;
use std::collections::VecDeque;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CollectorError {
NotAvailable,
Failed(String),
Disabled,
Timeout,
}
impl std::fmt::Display for CollectorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAvailable => write!(f, "Feature not available on this platform"),
Self::Failed(msg) => write!(f, "Collection failed: {msg}"),
Self::Disabled => write!(f, "Collector is disabled"),
Self::Timeout => write!(f, "Collection timed out"),
}
}
}
impl std::error::Error for CollectorError {}
pub trait CollectorBrick: Brick + Send + Sync {
type Metrics;
fn is_available(&self) -> bool;
fn collect(&mut self) -> Result<Self::Metrics, CollectorError>;
fn feature_gate(&self) -> Option<&'static str> {
None
}
fn collection_interval(&self) -> Duration {
Duration::from_millis(100)
}
}
pub trait AnalyzerBrick: Brick + Send + Sync {
type Input;
type Output;
fn analyze(&self, input: &Self::Input) -> Self::Output;
fn is_stale(&self, _age: Duration) -> bool {
false
}
}
pub trait PanelBrick: Brick + Send + Sync {
fn render(&self, width: u16, height: u16) -> Vec<String>;
fn title(&self) -> &str;
fn focusable(&self) -> bool {
true
}
fn explodable(&self) -> bool {
true
}
fn min_height(&self) -> u16 {
3
}
fn preferred_height_fraction(&self) -> f32 {
0.25
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PanelId {
Waveform,
Spectrogram,
Transcription,
Metrics,
VuMeter,
Status,
Custom(u32),
}
#[derive(Debug, Clone)]
pub struct PanelState {
pub focused: Option<PanelId>,
pub exploded: Option<PanelId>,
pub visible: Vec<PanelId>,
}
impl Default for PanelState {
fn default() -> Self {
Self {
focused: None,
exploded: None,
visible: vec![
PanelId::Waveform,
PanelId::Transcription,
PanelId::Metrics,
PanelId::Status,
],
}
}
}
impl PanelState {
#[must_use]
pub fn with_panels(panels: Vec<PanelId>) -> Self {
Self {
focused: panels.first().copied(),
exploded: None,
visible: panels,
}
}
pub fn focus_next(&mut self) {
if self.visible.is_empty() {
self.focused = None;
return;
}
let current_idx = self
.focused
.and_then(|f| self.visible.iter().position(|p| *p == f));
let next_idx = current_idx
.map(|i| (i + 1) % self.visible.len())
.unwrap_or(0);
self.focused = self.visible.get(next_idx).copied();
}
pub fn focus_prev(&mut self) {
if self.visible.is_empty() {
self.focused = None;
return;
}
let current_idx = self
.focused
.and_then(|f| self.visible.iter().position(|p| *p == f));
let prev_idx = current_idx
.map(|i| {
if i == 0 {
self.visible.len() - 1
} else {
i - 1
}
})
.unwrap_or(0);
self.focused = self.visible.get(prev_idx).copied();
}
pub fn toggle_explode(&mut self) {
if self.exploded.is_some() {
self.exploded = None;
} else {
self.exploded = self.focused;
}
}
#[must_use]
pub fn is_focused(&self, panel: PanelId) -> bool {
self.focused == Some(panel)
}
#[must_use]
pub fn is_exploded(&self, panel: PanelId) -> bool {
self.exploded == Some(panel)
}
#[must_use]
pub fn has_exploded(&self) -> bool {
self.exploded.is_some()
}
pub fn focus(&mut self, panel: PanelId) {
if self.visible.contains(&panel) {
self.focused = Some(panel);
}
}
pub fn add_panel(&mut self, panel: PanelId) {
if !self.visible.contains(&panel) {
self.visible.push(panel);
}
}
pub fn remove_panel(&mut self, panel: PanelId) {
self.visible.retain(|p| *p != panel);
if self.focused == Some(panel) {
self.focused = self.visible.first().copied();
}
if self.exploded == Some(panel) {
self.exploded = None;
}
}
}
#[derive(Debug, Clone)]
pub struct RingBuffer<T> {
data: VecDeque<T>,
capacity: usize,
}
impl<T> RingBuffer<T> {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
data: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, value: T) {
if self.data.len() >= self.capacity {
self.data.pop_front();
}
self.data.push_back(value);
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.data.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.data.iter_mut()
}
#[must_use]
pub fn len(&self) -> usize {
self.data.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
#[must_use]
pub fn capacity(&self) -> usize {
self.capacity
}
#[must_use]
pub fn last(&self) -> Option<&T> {
self.data.back()
}
#[must_use]
pub fn first(&self) -> Option<&T> {
self.data.front()
}
#[must_use]
pub fn get(&self, index: usize) -> Option<&T> {
self.data.get(index)
}
pub fn clear(&mut self) {
self.data.clear();
}
#[must_use]
pub fn is_full(&self) -> bool {
self.data.len() >= self.capacity
}
}
impl<T: Clone> RingBuffer<T> {
#[must_use]
pub fn to_vec(&self) -> Vec<T> {
self.data.iter().cloned().collect()
}
}
impl<T: Copy + Default> RingBuffer<T> {
pub fn fill_default(&mut self) {
while self.data.len() < self.capacity {
self.data.push_back(T::default());
}
}
}
impl<T: Copy + Into<f64>> RingBuffer<T> {
#[must_use]
pub fn average(&self) -> f64 {
if self.is_empty() {
return 0.0;
}
let sum: f64 = self.data.iter().map(|v| (*v).into()).sum();
sum / self.data.len() as f64
}
#[must_use]
pub fn min(&self) -> Option<f64> {
self.data
.iter()
.map(|v| (*v).into())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
#[must_use]
pub fn max(&self) -> Option<f64> {
self.data
.iter()
.map(|v| (*v).into())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CielabColor {
pub l: f32,
pub a: f32,
pub b: f32,
}
impl CielabColor {
#[must_use]
pub const fn new(l: f32, a: f32, b: f32) -> Self {
Self { l, a, b }
}
#[must_use]
pub fn lerp(&self, other: &Self, t: f32) -> Self {
let t = t.clamp(0.0, 1.0);
Self {
l: self.l + (other.l - self.l) * t,
a: self.a + (other.a - self.a) * t,
b: self.b + (other.b - self.b) * t,
}
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::many_single_char_names // x,y,z,r,g,b are standard color space variables
)]
pub fn to_rgb(&self) -> (u8, u8, u8) {
let fy = (self.l + 16.0) / 116.0;
let fx = self.a / 500.0 + fy;
let fz = fy - self.b / 200.0;
let xr = if fx.powi(3) > 0.008856 {
fx.powi(3)
} else {
(116.0 * fx - 16.0) / 903.3
};
let yr = if self.l > 7.9996 {
fy.powi(3)
} else {
self.l / 903.3
};
let zr = if fz.powi(3) > 0.008856 {
fz.powi(3)
} else {
(116.0 * fz - 16.0) / 903.3
};
let x = xr * 0.95047;
let y = yr * 1.0;
let z = zr * 1.08883;
let r = x * 3.2406 - y * 1.5372 - z * 0.4986;
let g = -x * 0.9689 + y * 1.8758 + z * 0.0415;
let b = x * 0.0557 - y * 0.2040 + z * 1.0570;
let gamma = |c: f32| -> f32 {
if c > 0.0031308 {
1.055 * c.powf(1.0 / 2.4) - 0.055
} else {
12.92 * c
}
};
let r = (gamma(r) * 255.0).clamp(0.0, 255.0) as u8;
let g = (gamma(g) * 255.0).clamp(0.0, 255.0) as u8;
let b = (gamma(b) * 255.0).clamp(0.0, 255.0) as u8;
(r, g, b)
}
#[must_use]
pub fn to_hex(&self) -> String {
let (r, g, b) = self.to_rgb();
format!("#{r:02x}{g:02x}{b:02x}")
}
#[must_use]
pub fn percent_gradient(percent: f32) -> Self {
let percent = percent.clamp(0.0, 1.0);
let green = Self::new(87.0, -86.0, 83.0);
let yellow = Self::new(97.0, -21.0, 94.0);
let red = Self::new(53.0, 80.0, 67.0);
if percent < 0.5 {
green.lerp(&yellow, percent * 2.0)
} else {
yellow.lerp(&red, (percent - 0.5) * 2.0)
}
}
#[must_use]
pub fn meter_gradient(level: f32) -> Self {
let level = level.clamp(0.0, 1.0);
let blue = Self::new(50.0, -10.0, -50.0);
let green = Self::new(87.0, -86.0, 83.0);
let yellow = Self::new(97.0, -21.0, 94.0);
let red = Self::new(53.0, 80.0, 67.0);
if level < 0.33 {
blue.lerp(&green, level * 3.0)
} else if level < 0.66 {
green.lerp(&yellow, (level - 0.33) * 3.0)
} else {
yellow.lerp(&red, (level - 0.66) * 3.0)
}
}
}
impl Default for CielabColor {
fn default() -> Self {
Self::new(50.0, 0.0, 0.0) }
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_ring_buffer_basic() {
let mut buf: RingBuffer<i32> = RingBuffer::new(3);
buf.push(1);
buf.push(2);
buf.push(3);
buf.push(4);
let values: Vec<_> = buf.iter().copied().collect();
assert_eq!(values, vec![2, 3, 4]);
}
#[test]
fn test_ring_buffer_capacity() {
let mut buf: RingBuffer<i32> = RingBuffer::new(5);
for i in 0..10 {
buf.push(i);
}
assert_eq!(buf.len(), 5);
assert_eq!(*buf.last().unwrap(), 9);
assert_eq!(*buf.first().unwrap(), 5);
}
#[test]
fn test_ring_buffer_to_vec() {
let mut buf: RingBuffer<i32> = RingBuffer::new(3);
buf.push(1);
buf.push(2);
buf.push(3);
buf.push(4);
assert_eq!(buf.to_vec(), vec![2, 3, 4]);
}
#[test]
fn test_ring_buffer_average() {
let mut buf: RingBuffer<f32> = RingBuffer::new(4);
buf.push(1.0);
buf.push(2.0);
buf.push(3.0);
buf.push(4.0);
assert!((buf.average() - 2.5).abs() < 0.001);
}
#[test]
fn test_ring_buffer_min_max() {
let mut buf: RingBuffer<f32> = RingBuffer::new(5);
buf.push(3.0);
buf.push(1.0);
buf.push(4.0);
buf.push(1.5);
buf.push(9.0);
assert!((buf.min().unwrap() - 1.0).abs() < 0.001);
assert!((buf.max().unwrap() - 9.0).abs() < 0.001);
}
#[test]
fn test_panel_state_focus() {
let mut state = PanelState::default();
state.focused = Some(PanelId::Waveform);
state.focus_next();
assert_eq!(state.focused, Some(PanelId::Transcription));
state.focus_next();
assert_eq!(state.focused, Some(PanelId::Metrics));
state.focus_prev();
assert_eq!(state.focused, Some(PanelId::Transcription));
}
#[test]
fn test_panel_state_focus_wrap() {
let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
state.focused = Some(PanelId::Metrics);
state.focus_next();
assert_eq!(state.focused, Some(PanelId::Waveform));
state.focus_prev();
assert_eq!(state.focused, Some(PanelId::Metrics));
}
#[test]
fn test_panel_state_explode() {
let mut state = PanelState::default();
state.focused = Some(PanelId::Transcription);
assert!(!state.has_exploded());
state.toggle_explode();
assert!(state.is_exploded(PanelId::Transcription));
assert!(state.has_exploded());
state.toggle_explode();
assert!(!state.has_exploded());
}
#[test]
fn test_panel_state_add_remove() {
let mut state = PanelState::default();
let custom = PanelId::Custom(42);
state.add_panel(custom);
assert!(state.visible.contains(&custom));
state.focus(custom);
assert_eq!(state.focused, Some(custom));
state.remove_panel(custom);
assert!(!state.visible.contains(&custom));
assert_ne!(state.focused, Some(custom));
}
#[test]
fn test_cielab_lerp() {
let green = CielabColor::new(87.0, -86.0, 83.0);
let red = CielabColor::new(53.0, 80.0, 67.0);
let mid = green.lerp(&red, 0.5);
assert!((mid.l - 70.0).abs() < 0.1);
assert!((mid.a - (-3.0)).abs() < 0.1);
}
#[test]
fn test_cielab_gradient() {
let start = CielabColor::percent_gradient(0.0);
let end = CielabColor::percent_gradient(1.0);
assert!(start.a < 0.0);
assert!(end.a > 0.0);
}
#[test]
fn test_cielab_to_rgb() {
let white = CielabColor::new(100.0, 0.0, 0.0);
let (r, g, b) = white.to_rgb();
assert!(r > 250);
assert!(g > 250);
assert!(b > 250);
let black = CielabColor::new(0.0, 0.0, 0.0);
let (r, g, b) = black.to_rgb();
assert!(r < 5);
assert!(g < 5);
assert!(b < 5);
}
#[test]
fn test_cielab_to_hex() {
let color = CielabColor::new(50.0, 0.0, 0.0);
let hex = color.to_hex();
assert!(hex.starts_with('#'));
assert_eq!(hex.len(), 7);
}
#[test]
fn test_cielab_meter_gradient() {
let low = CielabColor::meter_gradient(0.0);
let mid = CielabColor::meter_gradient(0.5);
let high = CielabColor::meter_gradient(1.0);
assert!(low.b < 0.0);
assert!(high.a > 0.0);
assert!(mid.l > 80.0);
}
#[test]
fn test_collector_error_display() {
assert_eq!(
CollectorError::NotAvailable.to_string(),
"Feature not available on this platform"
);
assert_eq!(
CollectorError::Failed("test".into()).to_string(),
"Collection failed: test"
);
}
#[test]
fn test_collector_error_disabled() {
assert_eq!(
CollectorError::Disabled.to_string(),
"Collector is disabled"
);
}
#[test]
fn test_collector_error_timeout() {
assert_eq!(CollectorError::Timeout.to_string(), "Collection timed out");
}
#[test]
fn test_collector_error_eq() {
assert_eq!(CollectorError::NotAvailable, CollectorError::NotAvailable);
assert_eq!(CollectorError::Disabled, CollectorError::Disabled);
assert_eq!(CollectorError::Timeout, CollectorError::Timeout);
assert_eq!(
CollectorError::Failed("a".into()),
CollectorError::Failed("a".into())
);
assert_ne!(
CollectorError::Failed("a".into()),
CollectorError::Failed("b".into())
);
}
#[test]
fn test_collector_error_clone() {
let err = CollectorError::Failed("test".into());
let cloned = err.clone();
assert_eq!(err, cloned);
}
#[test]
fn test_collector_error_is_error() {
let err = CollectorError::Timeout;
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_ring_buffer_is_empty() {
let buf: RingBuffer<i32> = RingBuffer::new(5);
assert!(buf.is_empty());
let mut buf2: RingBuffer<i32> = RingBuffer::new(5);
buf2.push(1);
assert!(!buf2.is_empty());
}
#[test]
fn test_ring_buffer_capacity_getter() {
let buf: RingBuffer<i32> = RingBuffer::new(10);
assert_eq!(buf.capacity(), 10);
}
#[test]
fn test_ring_buffer_first_last_empty() {
let buf: RingBuffer<i32> = RingBuffer::new(5);
assert!(buf.first().is_none());
assert!(buf.last().is_none());
}
#[test]
fn test_ring_buffer_get() {
let mut buf: RingBuffer<i32> = RingBuffer::new(5);
buf.push(10);
buf.push(20);
buf.push(30);
assert_eq!(buf.get(0), Some(&10));
assert_eq!(buf.get(1), Some(&20));
assert_eq!(buf.get(2), Some(&30));
assert_eq!(buf.get(3), None);
}
#[test]
fn test_ring_buffer_clear() {
let mut buf: RingBuffer<i32> = RingBuffer::new(5);
buf.push(1);
buf.push(2);
buf.push(3);
assert_eq!(buf.len(), 3);
buf.clear();
assert_eq!(buf.len(), 0);
assert!(buf.is_empty());
}
#[test]
fn test_ring_buffer_is_full() {
let mut buf: RingBuffer<i32> = RingBuffer::new(3);
assert!(!buf.is_full());
buf.push(1);
assert!(!buf.is_full());
buf.push(2);
assert!(!buf.is_full());
buf.push(3);
assert!(buf.is_full());
buf.push(4); assert!(buf.is_full());
}
#[test]
fn test_ring_buffer_iter_mut() {
let mut buf: RingBuffer<i32> = RingBuffer::new(5);
buf.push(1);
buf.push(2);
buf.push(3);
for val in buf.iter_mut() {
*val *= 2;
}
let values: Vec<_> = buf.iter().copied().collect();
assert_eq!(values, vec![2, 4, 6]);
}
#[test]
fn test_ring_buffer_fill_default() {
let mut buf: RingBuffer<i32> = RingBuffer::new(5);
buf.push(1);
buf.push(2);
assert_eq!(buf.len(), 2);
buf.fill_default();
assert_eq!(buf.len(), 5);
assert!(buf.is_full());
}
#[test]
fn test_ring_buffer_average_empty() {
let buf: RingBuffer<f32> = RingBuffer::new(5);
assert!((buf.average() - 0.0).abs() < 0.001);
}
#[test]
fn test_ring_buffer_min_max_empty() {
let buf: RingBuffer<f32> = RingBuffer::new(5);
assert!(buf.min().is_none());
assert!(buf.max().is_none());
}
#[test]
fn test_ring_buffer_clone() {
let mut buf: RingBuffer<i32> = RingBuffer::new(3);
buf.push(1);
buf.push(2);
let cloned = buf.clone();
assert_eq!(buf.len(), cloned.len());
assert_eq!(buf.to_vec(), cloned.to_vec());
}
#[test]
fn test_panel_id_eq() {
assert_eq!(PanelId::Waveform, PanelId::Waveform);
assert_eq!(PanelId::Custom(1), PanelId::Custom(1));
assert_ne!(PanelId::Custom(1), PanelId::Custom(2));
assert_ne!(PanelId::Waveform, PanelId::Spectrogram);
}
#[test]
fn test_panel_id_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(PanelId::Waveform);
set.insert(PanelId::Spectrogram);
set.insert(PanelId::Waveform); assert_eq!(set.len(), 2);
}
#[test]
fn test_panel_id_all_variants() {
let ids = [
PanelId::Waveform,
PanelId::Spectrogram,
PanelId::Transcription,
PanelId::Metrics,
PanelId::VuMeter,
PanelId::Status,
PanelId::Custom(0),
];
for id in ids {
assert_eq!(id, id);
}
}
#[test]
fn test_panel_state_default() {
let state = PanelState::default();
assert!(state.focused.is_none());
assert!(state.exploded.is_none());
assert_eq!(state.visible.len(), 4);
assert!(state.visible.contains(&PanelId::Waveform));
assert!(state.visible.contains(&PanelId::Transcription));
assert!(state.visible.contains(&PanelId::Metrics));
assert!(state.visible.contains(&PanelId::Status));
}
#[test]
fn test_panel_state_with_panels() {
let panels = vec![PanelId::Spectrogram, PanelId::VuMeter];
let state = PanelState::with_panels(panels);
assert_eq!(state.visible.len(), 2);
assert_eq!(state.focused, Some(PanelId::Spectrogram));
}
#[test]
fn test_panel_state_with_panels_empty() {
let state = PanelState::with_panels(vec![]);
assert!(state.visible.is_empty());
assert!(state.focused.is_none());
}
#[test]
fn test_panel_state_focus_next_empty() {
let mut state = PanelState::with_panels(vec![]);
state.focus_next();
assert!(state.focused.is_none());
}
#[test]
fn test_panel_state_focus_prev_empty() {
let mut state = PanelState::with_panels(vec![]);
state.focus_prev();
assert!(state.focused.is_none());
}
#[test]
fn test_panel_state_focus_next_no_current_focus() {
let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
state.focused = None;
state.focus_next();
assert_eq!(state.focused, Some(PanelId::Waveform));
}
#[test]
fn test_panel_state_focus_prev_no_current_focus() {
let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
state.focused = None;
state.focus_prev();
assert_eq!(state.focused, Some(PanelId::Waveform));
}
#[test]
fn test_panel_state_focus_prev_at_start() {
let mut state = PanelState::with_panels(vec![PanelId::Waveform, PanelId::Metrics]);
state.focused = Some(PanelId::Waveform);
state.focus_prev();
assert_eq!(state.focused, Some(PanelId::Metrics)); }
#[test]
fn test_panel_state_toggle_explode_no_focus() {
let mut state = PanelState::default();
state.focused = None;
state.toggle_explode();
assert!(state.exploded.is_none());
}
#[test]
fn test_panel_state_is_focused() {
let mut state = PanelState::default();
state.focused = Some(PanelId::Waveform);
assert!(state.is_focused(PanelId::Waveform));
assert!(!state.is_focused(PanelId::Metrics));
}
#[test]
fn test_panel_state_is_exploded() {
let mut state = PanelState::default();
state.exploded = Some(PanelId::Transcription);
assert!(state.is_exploded(PanelId::Transcription));
assert!(!state.is_exploded(PanelId::Waveform));
}
#[test]
fn test_panel_state_focus_invalid_panel() {
let mut state = PanelState::default();
state.focused = Some(PanelId::Waveform);
state.focus(PanelId::VuMeter);
assert_eq!(state.focused, Some(PanelId::Waveform));
}
#[test]
fn test_panel_state_add_panel_duplicate() {
let mut state = PanelState::default();
let initial_len = state.visible.len();
state.add_panel(PanelId::Waveform); assert_eq!(state.visible.len(), initial_len);
}
#[test]
fn test_panel_state_remove_panel_updates_focus() {
let mut state = PanelState::default();
state.focused = Some(PanelId::Metrics);
state.remove_panel(PanelId::Metrics);
assert_eq!(state.focused, Some(PanelId::Waveform));
}
#[test]
fn test_panel_state_remove_panel_clears_exploded() {
let mut state = PanelState::default();
state.exploded = Some(PanelId::Metrics);
state.remove_panel(PanelId::Metrics);
assert!(state.exploded.is_none());
}
#[test]
fn test_panel_state_clone() {
let state = PanelState::default();
let cloned = state.clone();
assert_eq!(state.visible.len(), cloned.visible.len());
}
#[test]
fn test_cielab_default() {
let color = CielabColor::default();
assert!((color.l - 50.0).abs() < 0.001);
assert!((color.a - 0.0).abs() < 0.001);
assert!((color.b - 0.0).abs() < 0.001);
}
#[test]
fn test_cielab_lerp_clamp() {
let c1 = CielabColor::new(0.0, 0.0, 0.0);
let c2 = CielabColor::new(100.0, 100.0, 100.0);
let result = c1.lerp(&c2, -0.5);
assert!((result.l - 0.0).abs() < 0.001);
let result = c1.lerp(&c2, 1.5);
assert!((result.l - 100.0).abs() < 0.001);
}
#[test]
fn test_cielab_percent_gradient_clamp() {
let color = CielabColor::percent_gradient(-0.5);
assert!(color.a < 0.0);
let color = CielabColor::percent_gradient(1.5);
assert!(color.a > 0.0); }
#[test]
fn test_cielab_percent_gradient_midpoint() {
let color = CielabColor::percent_gradient(0.5);
assert!(color.l > 90.0);
}
#[test]
fn test_cielab_meter_gradient_clamp() {
let low = CielabColor::meter_gradient(-1.0);
let high = CielabColor::meter_gradient(2.0);
assert!(low.b < 0.0); assert!(high.a > 0.0); }
#[test]
fn test_cielab_meter_gradient_transitions() {
let at_033 = CielabColor::meter_gradient(0.33);
let at_066 = CielabColor::meter_gradient(0.66);
assert!(at_033.l > 0.0);
assert!(at_066.l > 0.0);
}
#[test]
fn test_cielab_to_rgb_edge_cases() {
let dark = CielabColor::new(5.0, 0.0, 0.0);
let (r, g, b) = dark.to_rgb();
assert!(r < 20);
assert!(g < 20);
assert!(b < 20);
}
#[test]
fn test_cielab_eq() {
let c1 = CielabColor::new(50.0, 10.0, -20.0);
let c2 = CielabColor::new(50.0, 10.0, -20.0);
let c3 = CielabColor::new(51.0, 10.0, -20.0);
assert_eq!(c1, c2);
assert_ne!(c1, c3);
}
#[test]
fn test_cielab_clone() {
let c1 = CielabColor::new(75.0, 25.0, -50.0);
let c2 = c1;
assert_eq!(c1, c2);
}
#[test]
fn test_ring_buffer_f64() {
let mut buf: RingBuffer<f64> = RingBuffer::new(3);
buf.push(1.5);
buf.push(2.5);
buf.push(3.5);
assert!((buf.average() - 2.5).abs() < 0.001);
assert!((buf.min().unwrap() - 1.5).abs() < 0.001);
assert!((buf.max().unwrap() - 3.5).abs() < 0.001);
}
}