Skip to content

Commit 7bb2319

Browse files
feat(oscilloscope): stacked display mode (#17)
Signed-off-by: Maika Namuo <httpworldview@gmail.com>
1 parent 8787abe commit 7bb2319

7 files changed

Lines changed: 86 additions & 27 deletions

File tree

src/persistence/visuals.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ visual_settings!(OscilloscopeSettings from OscilloscopeConfig {
226226
channel_1: Channel, channel_2: Channel,
227227
} extra {
228228
persistence: f32 = 0.0,
229+
stacked: bool = false,
229230
});
230231

231232
visual_settings!(WaveformSettings from WaveformConfig {

src/ui/settings/oscilloscope.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22
// Copyright (C) 2026 Maika Namuo
33

4-
use super::widgets::{SliderRange, card, palette_card, pick, set_if_changed, update_f32_range};
4+
use super::widgets::{SliderRange, card, palette_card, pick, set_if_changed, toggle, update_f32_range};
55
use crate::persistence::settings::OscilloscopeSettings;
66
use crate::util::audio::Channel;
77
use crate::visuals::oscilloscope::processor::TriggerMode;
@@ -65,6 +65,7 @@ settings_messages!(pane, value {
6565
TriggerSource(Channel) => set_if_changed(&mut pane.settings.trigger_source, value);
6666
Channel1(Channel) => set_if_changed(&mut pane.settings.channel_1, value);
6767
Channel2(Channel) => set_if_changed(&mut pane.settings.channel_2, value);
68+
Stacked(bool) => set_if_changed(&mut pane.settings.stacked, value);
6869
});
6970

7071
impl Pane {
@@ -109,6 +110,7 @@ impl Pane {
109110
card("Signal", signal);
110111
card("Trigger", trigger);
111112
card("Display", controls!(8.0;
113+
toggle("Stacked", self.settings.stacked, Message::Stacked);
112114
slider!("Persistence", self.settings.persistence, PERSISTENCE_RANGE, Message::Persistence, "{:.2}");
113115
));
114116
palette_card(&self.palette, Message::Palette);

src/visuals/oscilloscope/processor.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
99
use std::collections::VecDeque;
1010
use std::sync::Arc;
1111

12-
const TRACE_COUNT: usize = 2;
12+
pub(in crate::visuals::oscilloscope) const TRACE_COUNT: usize = 2;
1313

1414
fn parabolic_refine(y_prev: f32, y_curr: f32, y_next: f32, tau: usize) -> f32 {
1515
let denom = y_prev - 2.0 * y_curr + y_next;
@@ -562,6 +562,7 @@ fn find_rising_zero_crossing(
562562
pub struct OscilloscopeSnapshot {
563563
pub epoch: u64,
564564
pub channels: usize,
565+
pub slots: [usize; TRACE_COUNT],
565566
pub samples: Vec<f32>,
566567
pub samples_per_channel: usize,
567568
}
@@ -722,6 +723,7 @@ impl OscilloscopeProcessor {
722723
capture,
723724
target,
724725
) {
726+
self.snapshot.slots[self.snapshot.channels] = slot;
725727
self.snapshot.channels += 1;
726728
}
727729
}

src/visuals/oscilloscope/render.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use iced::Rectangle;
55
use iced::advanced::graphics::Viewport;
66

7+
use super::processor::TRACE_COUNT;
78
use crate::util::color::rgba_with_alpha;
89
use crate::visuals::render::common::sdf_primitive;
910
use crate::visuals::render::common::{
@@ -16,8 +17,10 @@ pub struct OscilloscopeParams {
1617
pub bounds: Rectangle,
1718
pub channels: usize,
1819
pub samples_per_channel: usize,
20+
pub slots: [usize; TRACE_COUNT],
1921
pub samples: Vec<f32>,
20-
pub color: [f32; 4],
22+
pub colors: [[f32; 4]; TRACE_COUNT],
23+
pub stacked: bool,
2124
pub fill_alpha: f32,
2225
}
2326

@@ -29,9 +32,12 @@ impl OscilloscopePrimitive {
2932
const STROKE_WIDTH: f32 = 1.0;
3033

3134
let samples_per_channel = self.params.samples_per_channel;
32-
let channels = self.params.channels.max(1);
35+
let channels = self.params.channels.min(TRACE_COUNT);
3336

34-
if samples_per_channel < 2 || self.params.samples.len() < channels * samples_per_channel {
37+
if channels == 0
38+
|| samples_per_channel < 2
39+
|| self.params.samples.len() < channels * samples_per_channel
40+
{
3541
return;
3642
}
3743

@@ -40,7 +46,7 @@ impl OscilloscopePrimitive {
4046

4147
let layout = ChannelLayout::new(
4248
bounds,
43-
channels,
49+
if self.params.stacked { 1 } else { channels },
4450
VERTICAL_PADDING,
4551
CHANNEL_GAP,
4652
AMPLITUDE_SCALE,
@@ -51,15 +57,12 @@ impl OscilloscopePrimitive {
5157
let vertices = &mut scratch.vertices;
5258
let positions = &mut scratch.points;
5359

54-
for (channel_idx, channel_samples) in self
55-
.params
56-
.samples
57-
.chunks_exact(samples_per_channel)
58-
.take(channels)
59-
.enumerate()
60-
{
61-
let color = self.params.color;
62-
let center = layout.center_y(channel_idx);
60+
for i in 0..channels {
61+
let channel_idx = if self.params.stacked { channels - 1 - i } else { i };
62+
let start = channel_idx * samples_per_channel;
63+
let channel_samples = &self.params.samples[start..start + samples_per_channel];
64+
let color = self.params.colors[self.params.slots[channel_idx].min(TRACE_COUNT - 1)];
65+
let center = layout.center_y(if self.params.stacked { 0 } else { channel_idx });
6366

6467
positions.clear();
6568
positions.extend(channel_samples.iter().enumerate().map(|(i, &s)| {

src/visuals/oscilloscope/state.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// SPDX-License-Identifier: GPL-3.0-or-later
22
// Copyright (C) 2026 Maika Namuo
33

4-
use super::processor::OscilloscopeSnapshot;
4+
use super::processor::{OscilloscopeSnapshot, TRACE_COUNT};
55
use super::render::{OscilloscopeParams, OscilloscopePrimitive};
66
use crate::persistence::settings::OscilloscopeSettings;
77
use crate::util::color::color_to_rgba;
88
use crate::visuals::palettes;
99
use iced::Color;
1010

11-
const OSCILLOSCOPE_PALETTE_SIZE: usize = 1;
11+
const OSCILLOSCOPE_PALETTE_SIZE: usize = TRACE_COUNT;
1212
const MAX_PERSISTENCE: f32 = 0.98;
1313
const FILL_ALPHA: f32 = 0.15;
1414

@@ -17,6 +17,7 @@ pub(in crate::visuals) struct OscilloscopeState {
1717
snapshot: OscilloscopeSnapshot,
1818
pub(in crate::visuals) colors: [Color; OSCILLOSCOPE_PALETTE_SIZE],
1919
pub(in crate::visuals) persistence: f32,
20+
pub(in crate::visuals) stacked: bool,
2021
key: u64,
2122
}
2223

@@ -27,16 +28,18 @@ impl OscilloscopeState {
2728
snapshot: OscilloscopeSnapshot::default(),
2829
colors: palettes::oscilloscope::COLORS,
2930
persistence: defaults.persistence,
31+
stacked: defaults.stacked,
3032
key: crate::visuals::next_key(),
3133
}
3234
}
3335

34-
pub fn update_view_settings(&mut self, persistence: f32, reset_snapshot: bool) {
36+
pub fn update_view_settings(&mut self, persistence: f32, stacked: bool, reset_snapshot: bool) {
3537
self.persistence = if persistence.is_finite() {
3638
persistence.clamp(0.0, 1.0)
3739
} else {
3840
OscilloscopeSettings::default().persistence
3941
};
42+
self.stacked = stacked;
4043
if reset_snapshot {
4144
self.snapshot = OscilloscopeSnapshot::default();
4245
}
@@ -53,6 +56,7 @@ impl OscilloscopeState {
5356
&& snapshot.channels == self.snapshot.channels
5457
&& snapshot.samples_per_channel == self.snapshot.samples_per_channel
5558
&& snapshot.samples.len() == self.snapshot.samples.len()
59+
&& snapshot.slots[..snapshot.channels] == self.snapshot.slots[..self.snapshot.channels]
5660
{
5761
let persistence = self.persistence.clamp(0.0, MAX_PERSISTENCE);
5862
if persistence > f32::EPSILON {
@@ -80,8 +84,10 @@ impl OscilloscopeState {
8084
bounds,
8185
channels,
8286
samples_per_channel,
87+
slots: self.snapshot.slots,
8388
samples: self.snapshot.samples.clone(),
84-
color: color_to_rgba(self.colors[0]),
89+
colors: self.colors.map(color_to_rgba),
90+
stacked: self.stacked,
8591
fill_alpha: FILL_ALPHA,
8692
})
8793
}

src/visuals/palettes.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,12 @@ pub mod waveform {
117117

118118
pub mod oscilloscope {
119119
use super::Color;
120-
pub const COLORS: [Color; 1] = [Color::from_rgb8(0xFF, 0xFF, 0xFF)];
121-
pub const LABELS: &[&str] = &["Trace"];
122-
pub const DEFAULT_POSITIONS: [f32; COLORS.len()] = [0.0];
120+
pub const COLORS: [Color; 2] = [
121+
Color::from_rgb8(0xFF, 0xFF, 0xFF),
122+
Color::from_rgb8(0xFF, 0xFF, 0xFF),
123+
];
124+
pub const LABELS: &[&str] = &["Channel 1", "Channel 2"];
125+
pub const DEFAULT_POSITIONS: [f32; COLORS.len()] = [0.0, 1.0];
123126
}
124127

125128
pub mod stereometer {

src/visuals/registry.rs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,35 @@ use std::{cell::RefCell, rc::Rc};
2323

2424
type Shared<T> = Rc<RefCell<T>>;
2525

26+
// too many stops -> keep first N
27+
// too few stops -> copy provided, repeat last
2628
fn resolve_palette<const N: usize>(
2729
custom: Option<&PaletteSettings>,
2830
default: &[Color; N],
2931
) -> [Color; N] {
30-
custom
31-
.and_then(PaletteSettings::to_array)
32-
.unwrap_or(*default)
32+
let Some(custom) = custom else {
33+
return *default;
34+
};
35+
36+
if let Some(colors) = custom.to_array() {
37+
return colors;
38+
}
39+
40+
let mut colors = *default;
41+
let mut last = None;
42+
let mut used = 0;
43+
for (dst, stop) in colors.iter_mut().zip(&custom.stops) {
44+
let color = (*stop).into();
45+
*dst = color;
46+
last = Some(color);
47+
used += 1;
48+
}
49+
if let Some(color) = last
50+
&& used < N
51+
{
52+
colors[used..].fill(color);
53+
}
54+
colors
3355
}
3456

3557
macro_rules! visuals {
@@ -131,10 +153,10 @@ visuals! {
131153
oscilloscope::OscilloscopeProcessor, OscilloscopeConfig, OscilloscopeState;
132154
settings_cfg::OscilloscopeSettings;
133155
apply(p, s, set) { visuals!(@apply_config p, set); let reset = [set.channel_1, set.channel_2] == [Channel::None; 2];
134-
let mut st = s.borrow_mut(); st.update_view_settings(set.persistence, reset);
156+
let mut st = s.borrow_mut(); st.update_view_settings(set.persistence, set.stacked, reset);
135157
visuals!(@apply_palette st, set, &palettes::oscilloscope::COLORS); };
136158
export(p, s) { let st = s.borrow(); let mut out = settings_cfg::OscilloscopeSettings::from_config(&p.config());
137-
out.persistence = st.persistence;
159+
out.persistence = st.persistence; out.stacked = st.stacked;
138160
out.palette = visuals!(@export_palette &st.colors, &palettes::oscilloscope::COLORS); out };
139161

140162
Waveform(220.0, 220.0) =>
@@ -401,3 +423,23 @@ impl VisualManager {
401423
}
402424

403425
pub(crate) type VisualManagerHandle = Shared<VisualManager>;
426+
427+
#[cfg(test)]
428+
mod tests {
429+
use super::*;
430+
431+
#[test]
432+
fn short_palettes_extend_with_last_stop() {
433+
let a = Color::from_rgb8(1, 2, 3);
434+
let b = Color::from_rgb8(4, 5, 6);
435+
let palette = PaletteSettings {
436+
stops: vec![a.into(), b.into()],
437+
..Default::default()
438+
};
439+
440+
assert_eq!(
441+
resolve_palette(Some(&palette), &[Color::BLACK; 4]),
442+
[a, b, b, b]
443+
);
444+
}
445+
}

0 commit comments

Comments
 (0)