Add part of a simple peak meter widget for vizia
This commit is contained in:
parent
b0ba815514
commit
b8ff936b21
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2191,6 +2191,7 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"baseview",
|
"baseview",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
|
"femtovg",
|
||||||
"nih_plug",
|
"nih_plug",
|
||||||
"nih_plug_assets",
|
"nih_plug_assets",
|
||||||
"vizia",
|
"vizia",
|
||||||
|
|
|
@ -13,6 +13,8 @@ nih_plug_assets = { git = "https://github.com/robbert-vdh/nih_plug_assets.git" }
|
||||||
|
|
||||||
baseview = { git = "https://github.com/robbert-vdh/baseview.git", branch = "feature/mouse-event-modifiers" }
|
baseview = { git = "https://github.com/robbert-vdh/baseview.git", branch = "feature/mouse-event-modifiers" }
|
||||||
crossbeam = "0.8"
|
crossbeam = "0.8"
|
||||||
|
# Vizia doesn't re-export this, we will
|
||||||
|
femtovg = { version = "0.3.0", default-features = false, features = ["image-loading"] }
|
||||||
# This fork contains changed for better keyboard modifier handling and DPI
|
# This fork contains changed for better keyboard modifier handling and DPI
|
||||||
# scaling
|
# scaling
|
||||||
vizia = { git = "https://github.com/robbert-vdh/vizia.git", branch = "feature/baseview-modifiers", default_features = false, features = ["baseview", "clipboard"] }
|
vizia = { git = "https://github.com/robbert-vdh/vizia.git", branch = "feature/baseview-modifiers", default_features = false, features = ["baseview", "clipboard"] }
|
||||||
|
|
|
@ -36,3 +36,17 @@ param-slider .value-entry .caret {
|
||||||
param-slider .value-entry .selection {
|
param-slider .value-entry .selection {
|
||||||
background-color: #0a0a0a30;
|
background-color: #0a0a0a30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peak-meter {
|
||||||
|
height: 30px;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
peak-meter .bar {
|
||||||
|
height: 50%;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #0a0a0a;
|
||||||
|
}
|
||||||
|
peak-meter .ticks {
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ use std::sync::Arc;
|
||||||
use vizia::{Application, Color, Context, Entity, Model, PropSet, WindowDescription};
|
use vizia::{Application, Color, Context, Entity, Model, PropSet, WindowDescription};
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
|
pub use femtovg;
|
||||||
pub use vizia;
|
pub use vizia;
|
||||||
|
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
|
@ -12,9 +12,11 @@ use std::sync::Arc;
|
||||||
use vizia::{Context, Model};
|
use vizia::{Context, Model};
|
||||||
|
|
||||||
mod param_slider;
|
mod param_slider;
|
||||||
|
mod peak_meter;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use param_slider::{ParamSlider, ParamSliderExt, ParamSliderStyle};
|
pub use param_slider::{ParamSlider, ParamSliderExt, ParamSliderStyle};
|
||||||
|
pub use peak_meter::PeakMeter;
|
||||||
|
|
||||||
/// Register the default theme for the widgets exported by this module. This is automatically called
|
/// Register the default theme for the widgets exported by this module. This is automatically called
|
||||||
/// for you when using [`create_vizia_editor()`][super::create_vizia_editor()].
|
/// for you when using [`create_vizia_editor()`][super::create_vizia_editor()].
|
||||||
|
|
220
nih_plug_vizia/src/widgets/peak_meter.rs
Normal file
220
nih_plug_vizia/src/widgets/peak_meter.rs
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
//! A super simple peak meter widget.
|
||||||
|
|
||||||
|
use femtovg::{Paint, Path};
|
||||||
|
use nih_plug::prelude::util;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
use vizia::*;
|
||||||
|
|
||||||
|
/// The thickness of a tick inside of the peak meter's bar.
|
||||||
|
const TICK_WIDTH: f32 = 1.0;
|
||||||
|
/// The gap between individual ticks.
|
||||||
|
const TICK_GAP: f32 = 1.0;
|
||||||
|
|
||||||
|
/// The decibel value corresponding to the very left of the bar.
|
||||||
|
const MIN_TICK: f32 = -90.0;
|
||||||
|
/// The decibel value corresponding to the very right of the bar.
|
||||||
|
const MAX_TICK: f32 = 20.0;
|
||||||
|
|
||||||
|
/// A simple horizontal peak meter.
|
||||||
|
///
|
||||||
|
/// TODO: There are currently no styling options at all
|
||||||
|
/// TODO: Vertical peak meter, this is just a proof of concept to fit the gain GUI example.
|
||||||
|
pub struct PeakMeter;
|
||||||
|
|
||||||
|
/// The bar bit for the peak meter, manually drawn using vertical lines.
|
||||||
|
struct PeakMeterInner<L, P>
|
||||||
|
where
|
||||||
|
L: Lens<Target = f32>,
|
||||||
|
P: Lens<Target = f32>,
|
||||||
|
{
|
||||||
|
level_dbfs: L,
|
||||||
|
peak_dbfs: P,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PeakMeter {
|
||||||
|
/// Creates a new [`PeakMeter`] for the given value in decibel, optionally holding the peak
|
||||||
|
/// value for a certain amount of time.
|
||||||
|
///
|
||||||
|
/// See [`PeakMeterExt`] for additonal options.
|
||||||
|
pub fn new<L>(
|
||||||
|
cx: &mut Context,
|
||||||
|
level_dbfs: L,
|
||||||
|
hold_time: Option<Duration>,
|
||||||
|
) -> Handle<'_, PeakMeter>
|
||||||
|
where
|
||||||
|
L: Lens<Target = f32>,
|
||||||
|
{
|
||||||
|
Self.build2(cx, |cx| {
|
||||||
|
// Now for something that may be illegal under some jurisdictions. If a hold time is
|
||||||
|
// given, then we'll build a new lens that always gives the held peak level for the
|
||||||
|
// current moment in time by mutating some values captured into the mapping closure.
|
||||||
|
let peak_dbfs = match hold_time {
|
||||||
|
Some(hold_time) => {
|
||||||
|
let held_peak_value_db = Cell::new(f32::MIN);
|
||||||
|
let last_held_peak_value: Cell<Option<Instant>> = Cell::new(None);
|
||||||
|
level_dbfs.clone().map(move |level| -> f32 {
|
||||||
|
let mut peak_level = held_peak_value_db.get();
|
||||||
|
let peak_time = last_held_peak_value.get();
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
if *level >= peak_level
|
||||||
|
|| peak_time.is_none()
|
||||||
|
|| now > peak_time.unwrap() + hold_time
|
||||||
|
{
|
||||||
|
peak_level = *level;
|
||||||
|
held_peak_value_db.set(peak_level);
|
||||||
|
last_held_peak_value.set(Some(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
peak_level
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => level_dbfs
|
||||||
|
.clone()
|
||||||
|
.map(|_level| -> f32 { util::MINUS_INFINITY_DB }),
|
||||||
|
};
|
||||||
|
|
||||||
|
PeakMeterInner {
|
||||||
|
level_dbfs,
|
||||||
|
peak_dbfs,
|
||||||
|
}
|
||||||
|
.build(cx)
|
||||||
|
.class("bar");
|
||||||
|
|
||||||
|
// TODO: Ticks
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for PeakMeter {
|
||||||
|
fn element(&self) -> Option<String> {
|
||||||
|
Some(String::from("peak-meter"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, P> View for PeakMeterInner<L, P>
|
||||||
|
where
|
||||||
|
L: Lens<Target = f32>,
|
||||||
|
P: Lens<Target = f32>,
|
||||||
|
{
|
||||||
|
fn draw(&self, cx: &mut Context, canvas: &mut Canvas) {
|
||||||
|
let level_dbfs = *self.level_dbfs.get(cx);
|
||||||
|
let peak_dbfs = *self.peak_dbfs.get(cx);
|
||||||
|
|
||||||
|
// These basics are taken directly from the default implementation of this function
|
||||||
|
let entity = cx.current;
|
||||||
|
let bounds = cx.cache.get_bounds(entity);
|
||||||
|
if bounds.w == 0.0 || bounds.h == 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: It would be cool to allow the text color property to control the gradient here. For
|
||||||
|
// now we'll only support basic background colors and borders.
|
||||||
|
let background_color = cx
|
||||||
|
.style
|
||||||
|
.background_color
|
||||||
|
.get(entity)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let border_color = cx
|
||||||
|
.style
|
||||||
|
.border_color
|
||||||
|
.get(entity)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let opacity = cx.cache.get_opacity(entity);
|
||||||
|
let mut background_color: femtovg::Color = background_color.into();
|
||||||
|
background_color.set_alphaf(background_color.a * opacity);
|
||||||
|
let mut border_color: femtovg::Color = border_color.into();
|
||||||
|
border_color.set_alphaf(border_color.a * opacity);
|
||||||
|
|
||||||
|
let border_width = match cx
|
||||||
|
.style
|
||||||
|
.border_width
|
||||||
|
.get(entity)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
Units::Pixels(val) => val,
|
||||||
|
Units::Percentage(val) => bounds.w.min(bounds.h) * (val / 100.0),
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut path = Path::new();
|
||||||
|
{
|
||||||
|
let x = bounds.x + border_width / 2.0;
|
||||||
|
let y = bounds.y + border_width / 2.0;
|
||||||
|
let w = bounds.w - border_width;
|
||||||
|
let h = bounds.h - border_width;
|
||||||
|
path.move_to(x, y);
|
||||||
|
path.line_to(x, y + h);
|
||||||
|
path.line_to(x + w, y + h);
|
||||||
|
path.line_to(x + w, y);
|
||||||
|
path.line_to(x, y);
|
||||||
|
path.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill with background color
|
||||||
|
let paint = Paint::color(background_color);
|
||||||
|
canvas.fill_path(&mut path, paint);
|
||||||
|
|
||||||
|
// And now for the fun stuff. We'll try to not overlap the border, but we'll draw that last
|
||||||
|
// just in case.
|
||||||
|
let bar_bounds = bounds.shrink(border_width / 2.0);
|
||||||
|
let bar_ticks_start_x = bar_bounds.left().floor() as i32;
|
||||||
|
let bar_ticks_end_x = bar_bounds.right().ceil() as i32;
|
||||||
|
let bar_tick_coordinates =
|
||||||
|
(bar_ticks_start_x..bar_ticks_end_x).step_by((TICK_WIDTH + TICK_GAP).round() as usize);
|
||||||
|
for tick_x in bar_tick_coordinates {
|
||||||
|
let tick_fraction =
|
||||||
|
(tick_x - bar_ticks_start_x) as f32 / (bar_ticks_end_x - bar_ticks_start_x) as f32;
|
||||||
|
let tick_db = (tick_fraction * (MAX_TICK - MIN_TICK)) + MIN_TICK;
|
||||||
|
if tick_db > level_dbfs {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// femtovg draws paths centered on these coordinates, so in order to be pixel perfect we
|
||||||
|
// need to account for that. Otherwise the ticks will be 2px wide instead of 1px.
|
||||||
|
let mut path = Path::new();
|
||||||
|
path.move_to(tick_x as f32 + 0.5, bar_bounds.top());
|
||||||
|
path.line_to(tick_x as f32 + 0.5, bar_bounds.bottom());
|
||||||
|
|
||||||
|
let grayscale_color = 0.3 + ((1.0 - tick_fraction) * 0.5);
|
||||||
|
let mut paint = Paint::color(femtovg::Color::rgbaf(
|
||||||
|
grayscale_color,
|
||||||
|
grayscale_color,
|
||||||
|
grayscale_color,
|
||||||
|
opacity,
|
||||||
|
));
|
||||||
|
paint.set_line_width(TICK_WIDTH);
|
||||||
|
canvas.stroke_path(&mut path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the hold peak value if the hold time option has been set
|
||||||
|
let db_to_x_coord = |db: f32| {
|
||||||
|
let tick_fraction = (db - MIN_TICK) / (MAX_TICK - MIN_TICK);
|
||||||
|
bar_ticks_start_x as f32
|
||||||
|
+ ((bar_ticks_end_x - bar_ticks_start_x) as f32 * tick_fraction).round()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (MIN_TICK..MAX_TICK).contains(&peak_dbfs) {
|
||||||
|
// femtovg draws paths centered on these coordinates, so in order to be pixel perfect we
|
||||||
|
// need to account for that. Otherwise the ticks will be 2px wide instead of 1px.
|
||||||
|
let peak_x = db_to_x_coord(peak_dbfs);
|
||||||
|
let mut path = Path::new();
|
||||||
|
path.move_to(peak_x + 0.5, bar_bounds.top());
|
||||||
|
path.line_to(peak_x + 0.5, bar_bounds.bottom());
|
||||||
|
|
||||||
|
let mut paint = Paint::color(femtovg::Color::rgbaf(0.3, 0.3, 0.3, opacity));
|
||||||
|
paint.set_line_width(TICK_WIDTH);
|
||||||
|
canvas.stroke_path(&mut path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw border last
|
||||||
|
let mut paint = Paint::color(border_color);
|
||||||
|
paint.set_line_width(border_width);
|
||||||
|
canvas.stroke_path(&mut path, paint);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
use atomic_float::AtomicF32;
|
use atomic_float::AtomicF32;
|
||||||
use nih_plug::prelude::Editor;
|
use nih_plug::prelude::{util, Editor};
|
||||||
use nih_plug_vizia::vizia::*;
|
use nih_plug_vizia::vizia::*;
|
||||||
use nih_plug_vizia::widgets::*;
|
use nih_plug_vizia::widgets::*;
|
||||||
use nih_plug_vizia::{assets, create_vizia_editor, ViziaState};
|
use nih_plug_vizia::{assets, create_vizia_editor, ViziaState};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::GainParams;
|
use crate::GainParams;
|
||||||
|
|
||||||
|
@ -48,23 +50,21 @@ pub(crate) fn create(
|
||||||
.height(Pixels(50.0))
|
.height(Pixels(50.0))
|
||||||
.child_top(Stretch(1.0))
|
.child_top(Stretch(1.0))
|
||||||
.child_bottom(Pixels(0.0));
|
.child_bottom(Pixels(0.0));
|
||||||
|
|
||||||
// NOTE: VIZIA adds 1 pixel of additional height to these labels, so we'll need to
|
// NOTE: VIZIA adds 1 pixel of additional height to these labels, so we'll need to
|
||||||
// compensate for that
|
// compensate for that
|
||||||
Label::new(cx, "Gain").bottom(Pixels(-1.0));
|
Label::new(cx, "Gain").bottom(Pixels(-1.0));
|
||||||
|
ParamSlider::new(cx, Data::params, |params| ¶ms.gain)
|
||||||
|
.set_style(ParamSliderStyle::FromLeft);
|
||||||
|
|
||||||
VStack::new(cx, |cx| {
|
PeakMeter::new(
|
||||||
ParamSlider::new(cx, Data::params, |params| ¶ms.gain);
|
cx,
|
||||||
ParamSlider::new(cx, Data::params, |params| ¶ms.gain)
|
Data::peak_meter
|
||||||
.set_style(ParamSliderStyle::FromLeft);
|
.map(|peak_meter| util::gain_to_db(peak_meter.load(Ordering::Relaxed))),
|
||||||
ParamSlider::new(cx, Data::params, |params| ¶ms.foo);
|
Some(Duration::from_millis(600)),
|
||||||
ParamSlider::new(cx, Data::params, |params| ¶ms.foo)
|
)
|
||||||
.set_style(ParamSliderStyle::CurrentStep);
|
// This is how adding padding works in vizia
|
||||||
ParamSlider::new(cx, Data::params, |params| ¶ms.foo)
|
.top(Pixels(10.0));
|
||||||
.set_style(ParamSliderStyle::CurrentStepLabeled);
|
|
||||||
})
|
|
||||||
.row_between(Pixels(5.0));
|
|
||||||
|
|
||||||
// TODO: Add a peak meter
|
|
||||||
})
|
})
|
||||||
.row_between(Pixels(0.0))
|
.row_between(Pixels(0.0))
|
||||||
.child_left(Stretch(1.0))
|
.child_left(Stretch(1.0))
|
||||||
|
|
Loading…
Reference in a new issue