2022-03-19 11:17:13 +11:00
|
|
|
//! A slider that integrates with NIH-plug's [`Param`] types.
|
|
|
|
|
2022-11-07 05:13:08 +11:00
|
|
|
use nih_plug::prelude::Param;
|
2022-06-18 09:59:53 +10:00
|
|
|
use vizia::prelude::*;
|
2022-03-19 11:17:13 +11:00
|
|
|
|
2022-11-07 05:13:08 +11:00
|
|
|
use super::param_base::ParamWidgetBase;
|
2022-03-19 11:17:13 +11:00
|
|
|
use super::util::{self, ModifiersExt};
|
|
|
|
|
|
|
|
/// When shift+dragging a parameter, one pixel dragged corresponds to this much change in the
|
2022-09-22 02:32:24 +10:00
|
|
|
/// normalized parameter.
|
2022-03-19 11:17:13 +11:00
|
|
|
const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1;
|
|
|
|
|
2022-03-22 04:36:41 +11:00
|
|
|
/// A slider that integrates with NIH-plug's [`Param`] types. Use the
|
2022-03-27 12:32:45 +11:00
|
|
|
/// [`set_style()`][ParamSliderExt::set_style()] method to change how the value gets displayed.
|
2022-09-22 02:30:02 +10:00
|
|
|
#[derive(Lens)]
|
2022-03-19 11:17:13 +11:00
|
|
|
pub struct ParamSlider {
|
2022-11-07 05:13:08 +11:00
|
|
|
param_base: ParamWidgetBase,
|
2022-03-19 11:17:13 +11:00
|
|
|
|
2022-11-07 23:37:56 +11:00
|
|
|
/// Will be set to `true` when the field gets Alt+Click'ed which will replace the label with a
|
|
|
|
/// text box.
|
|
|
|
text_input_active: bool,
|
2022-03-19 11:17:13 +11:00
|
|
|
/// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a
|
|
|
|
/// text value should not initiate a drag.
|
|
|
|
drag_active: bool,
|
2022-03-19 12:18:30 +11:00
|
|
|
/// We keep track of the start coordinate and normalized value when holding down Shift while
|
|
|
|
/// dragging for higher precision dragging. This is a `None` value when granular dragging is not
|
|
|
|
/// active.
|
|
|
|
granular_drag_start_x_value: Option<(f32, f32)>,
|
2022-09-22 02:30:02 +10:00
|
|
|
|
2022-11-07 23:37:56 +11:00
|
|
|
// These fields are set through modifiers:
|
2022-11-09 07:20:40 +11:00
|
|
|
/// Whether or not to listen to scroll events for changing the parameter's value in steps.
|
|
|
|
use_scroll_wheel: bool,
|
|
|
|
/// The number of (fractional) scrolled lines that have not yet been turned into parameter
|
|
|
|
/// change events. This is needed to support trackpads with smooth scrolling.
|
|
|
|
scrolled_lines: f32,
|
2022-09-22 02:30:02 +10:00
|
|
|
/// What style to use for the slider.
|
|
|
|
style: ParamSliderStyle,
|
2022-11-08 01:08:50 +11:00
|
|
|
/// A specific label to use instead of displaying the parameter's value.
|
|
|
|
label_override: Option<String>,
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
|
|
|
|
2022-03-20 04:49:49 +11:00
|
|
|
/// How the [`ParamSlider`] should display its values. Set this using
|
2022-03-27 12:32:45 +11:00
|
|
|
/// [`ParamSliderExt::set_style()`].
|
2022-03-20 04:49:49 +11:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Data)]
|
|
|
|
pub enum ParamSliderStyle {
|
|
|
|
/// Visualize the offset from the default value for continuous parameters with a default value
|
|
|
|
/// at around half of its range, fill the bar from the left for discrete parameters and
|
2022-09-22 02:32:24 +10:00
|
|
|
/// continuous parameters without centered default values.
|
2022-03-20 04:49:49 +11:00
|
|
|
Centered,
|
|
|
|
/// Always fill the bar starting from the left.
|
|
|
|
FromLeft,
|
2022-03-23 05:44:40 +11:00
|
|
|
/// Show the current step instead of filling a portion of the bar, useful for discrete
|
|
|
|
/// parameters. Set `even` to `true` to distribute the ticks evenly instead of following the
|
|
|
|
/// parameter's distribution. This can be desireable because discrete parameters have smaller
|
|
|
|
/// ranges near the edges (they'll span only half the range, which can make the display look
|
|
|
|
/// odd).
|
|
|
|
CurrentStep { even: bool },
|
2022-03-20 05:42:50 +11:00
|
|
|
/// The same as `CurrentStep`, but overlay the labels over the steps instead of showing the
|
|
|
|
/// active value. Only useful for discrete parameters with two, maybe three possible values.
|
2022-03-23 05:44:40 +11:00
|
|
|
CurrentStepLabeled { even: bool },
|
2022-03-20 04:49:49 +11:00
|
|
|
}
|
|
|
|
|
2022-03-20 01:28:58 +11:00
|
|
|
enum ParamSliderEvent {
|
|
|
|
/// Text input has been cancelled without submitting a new value.
|
|
|
|
CancelTextInput,
|
2022-09-22 02:30:02 +10:00
|
|
|
/// A new value has been sent by the text input dialog after pressing Enter.
|
2022-03-20 01:28:58 +11:00
|
|
|
TextInput(String),
|
|
|
|
}
|
|
|
|
|
2022-03-19 11:17:13 +11:00
|
|
|
impl ParamSlider {
|
2022-09-22 02:30:02 +10:00
|
|
|
/// Creates a new [`ParamSlider`] for the given parameter. To accommodate VIZIA's mapping system,
|
2022-03-19 11:17:13 +11:00
|
|
|
/// you'll need to provide a lens containing your `Params` implementation object (check out how
|
2022-03-21 23:09:51 +11:00
|
|
|
/// the `Data` struct is used in `gain_gui_vizia`) and a projection function that maps the
|
|
|
|
/// `Params` object to the parameter you want to display a widget for. Parameter changes are
|
|
|
|
/// handled by emitting [`ParamEvent`][super::ParamEvent]s which are automatically handled by
|
|
|
|
/// the VIZIA wrapper.
|
2022-03-20 04:49:49 +11:00
|
|
|
///
|
2022-09-22 02:32:24 +10:00
|
|
|
/// See [`ParamSliderExt`] for additional options.
|
2022-11-07 05:13:08 +11:00
|
|
|
pub fn new<L, Params, P, FMap>(
|
|
|
|
cx: &mut Context,
|
|
|
|
params: L,
|
|
|
|
params_to_param: FMap,
|
|
|
|
) -> Handle<Self>
|
2022-03-19 11:17:13 +11:00
|
|
|
where
|
2022-07-24 02:58:41 +10:00
|
|
|
L: Lens<Target = Params> + Clone,
|
2022-03-19 11:17:13 +11:00
|
|
|
Params: 'static,
|
2022-11-07 05:13:08 +11:00
|
|
|
P: Param + 'static,
|
|
|
|
FMap: Fn(&Params) -> &P + Copy + 'static,
|
2022-03-19 11:17:13 +11:00
|
|
|
{
|
|
|
|
// We'll visualize the difference between the current value and the default value if the
|
|
|
|
// default value lies somewhere in the middle and the parameter is continuous. Otherwise
|
2022-09-22 02:30:02 +10:00
|
|
|
// this approach looks a bit jarring.
|
2022-03-19 11:17:13 +11:00
|
|
|
Self {
|
2022-11-07 05:13:08 +11:00
|
|
|
param_base: ParamWidgetBase::new(cx, params.clone(), params_to_param),
|
2022-03-19 11:17:13 +11:00
|
|
|
|
2022-11-07 23:37:56 +11:00
|
|
|
text_input_active: false,
|
2022-03-19 11:17:13 +11:00
|
|
|
drag_active: false,
|
2022-03-19 12:18:30 +11:00
|
|
|
granular_drag_start_x_value: None,
|
2022-09-22 02:30:02 +10:00
|
|
|
|
2022-11-09 07:20:40 +11:00
|
|
|
use_scroll_wheel: true,
|
|
|
|
scrolled_lines: 0.0,
|
2022-09-22 02:30:02 +10:00
|
|
|
style: ParamSliderStyle::Centered,
|
2022-11-08 01:08:50 +11:00
|
|
|
label_override: None,
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
2022-11-07 05:13:08 +11:00
|
|
|
.build(
|
|
|
|
cx,
|
|
|
|
ParamWidgetBase::view(params, params_to_param, move |cx, param_data| {
|
|
|
|
Binding::new(cx, ParamSlider::style, move |cx, style| {
|
|
|
|
let style = style.get(cx);
|
|
|
|
|
2022-11-07 23:35:48 +11:00
|
|
|
// Needs to be moved into the below closures, and it can't be `Copy`
|
|
|
|
let param_data = param_data.clone();
|
2022-11-07 05:13:08 +11:00
|
|
|
|
|
|
|
// Can't use `.to_string()` here as that would include the modulation.
|
|
|
|
let unmodulated_normalized_value_lens =
|
|
|
|
param_data.make_lens(|param| param.unmodulated_normalized_value());
|
|
|
|
let display_value_lens = param_data.make_lens(|param| {
|
|
|
|
param.normalized_value_to_string(param.unmodulated_normalized_value(), true)
|
|
|
|
});
|
|
|
|
|
|
|
|
// The resulting tuple `(start_t, delta)` corresponds to the start and the
|
|
|
|
// signed width of the bar. `start_t` is in `[0, 1]`, and `delta` is in
|
|
|
|
// `[-1, 1]`.
|
2022-11-07 23:35:48 +11:00
|
|
|
let fill_start_delta_lens = {
|
2022-11-07 05:13:08 +11:00
|
|
|
let param_data = param_data.clone();
|
2022-11-07 23:35:48 +11:00
|
|
|
unmodulated_normalized_value_lens.map(move |current_value| {
|
|
|
|
Self::compute_fill_start_delta(
|
|
|
|
style,
|
|
|
|
param_data.param(),
|
|
|
|
*current_value,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
};
|
2022-11-07 05:13:08 +11:00
|
|
|
|
|
|
|
// If the parameter is being modulated by the host (this only works for CLAP
|
|
|
|
// plugins with hosts that support this), then this is the difference
|
|
|
|
// between the 'true' value and the current value after modulation has been
|
2022-11-07 23:35:48 +11:00
|
|
|
// applied. This follows the same format as `fill_start_delta_lens`.
|
2022-11-07 05:13:08 +11:00
|
|
|
let modulation_start_delta_lens = param_data.make_lens(move |param| {
|
2022-11-07 23:35:48 +11:00
|
|
|
Self::compute_modulation_fill_start_delta(style, param)
|
2022-11-07 05:13:08 +11:00
|
|
|
});
|
|
|
|
|
2022-11-07 23:35:48 +11:00
|
|
|
// This is used to draw labels for `CurrentStepLabeled`
|
|
|
|
let make_preview_value_lens = {
|
|
|
|
let param_data = param_data.clone();
|
|
|
|
move |normalized_value| {
|
|
|
|
param_data.make_lens(move |param| {
|
|
|
|
param.normalized_value_to_string(normalized_value, true)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-11-07 05:13:08 +11:00
|
|
|
// Only draw the text input widget when it gets focussed. Otherwise, overlay the
|
|
|
|
// label with the slider. Creating the textbox based on
|
|
|
|
// `ParamSliderInternal::text_input_active` lets us focus the textbox when it gets
|
|
|
|
// created.
|
|
|
|
Binding::new(
|
|
|
|
cx,
|
|
|
|
ParamSlider::text_input_active,
|
|
|
|
move |cx, text_input_active| {
|
|
|
|
if text_input_active.get(cx) {
|
2022-11-07 23:35:48 +11:00
|
|
|
Self::text_input_view(cx, display_value_lens.clone());
|
2022-11-07 05:13:08 +11:00
|
|
|
} else {
|
2022-11-07 23:35:48 +11:00
|
|
|
// All of this data needs to be moved into the `ZStack` closure, and
|
|
|
|
// the `Map` lens combinator isn't `Copy`
|
|
|
|
let param_data = param_data.clone();
|
2022-11-07 05:13:08 +11:00
|
|
|
let fill_start_delta_lens = fill_start_delta_lens.clone();
|
|
|
|
let modulation_start_delta_lens =
|
|
|
|
modulation_start_delta_lens.clone();
|
2022-11-07 23:35:48 +11:00
|
|
|
let display_value_lens = display_value_lens.clone();
|
2022-11-07 05:13:08 +11:00
|
|
|
let make_preview_value_lens = make_preview_value_lens.clone();
|
|
|
|
|
|
|
|
ZStack::new(cx, move |cx| {
|
2022-11-07 23:35:48 +11:00
|
|
|
Self::slider_fill_view(
|
|
|
|
cx,
|
|
|
|
fill_start_delta_lens,
|
|
|
|
modulation_start_delta_lens,
|
|
|
|
);
|
|
|
|
Self::slider_label_view(
|
|
|
|
cx,
|
|
|
|
param_data.param(),
|
|
|
|
style,
|
|
|
|
display_value_lens,
|
|
|
|
make_preview_value_lens,
|
2022-11-08 01:08:50 +11:00
|
|
|
ParamSlider::label_override,
|
2022-11-07 23:35:48 +11:00
|
|
|
);
|
2022-11-07 05:13:08 +11:00
|
|
|
})
|
|
|
|
.hoverable(false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}),
|
|
|
|
)
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
2022-03-23 09:28:15 +11:00
|
|
|
|
2022-11-07 23:35:48 +11:00
|
|
|
/// Create a text input that's shown in place of the slider.
|
|
|
|
fn text_input_view(cx: &mut Context, display_value_lens: impl Lens<Target = String>) {
|
|
|
|
Textbox::new(cx, display_value_lens)
|
|
|
|
.class("value-entry")
|
|
|
|
.on_submit(|cx, string, success| {
|
|
|
|
if success {
|
|
|
|
cx.emit(ParamSliderEvent::TextInput(string))
|
|
|
|
} else {
|
|
|
|
cx.emit(ParamSliderEvent::CancelTextInput);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.on_build(|cx| {
|
|
|
|
cx.emit(TextEvent::StartEdit);
|
|
|
|
cx.emit(TextEvent::SelectAll);
|
|
|
|
})
|
|
|
|
// `.child_space(Stretch(1.0))` no longer works
|
|
|
|
.class("align_center")
|
|
|
|
.child_top(Stretch(1.0))
|
|
|
|
.child_bottom(Stretch(1.0))
|
|
|
|
.height(Stretch(1.0))
|
|
|
|
.width(Stretch(1.0));
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create the fill part of the slider.
|
|
|
|
fn slider_fill_view(
|
|
|
|
cx: &mut Context,
|
|
|
|
fill_start_delta_lens: impl Lens<Target = (f32, f32)>,
|
|
|
|
modulation_start_delta_lens: impl Lens<Target = (f32, f32)>,
|
|
|
|
) {
|
|
|
|
// The filled bar portion. This can be visualized in a couple different ways depending on
|
|
|
|
// the current style property. See [`ParamSliderStyle`].
|
|
|
|
Element::new(cx)
|
|
|
|
.class("fill")
|
|
|
|
.height(Stretch(1.0))
|
|
|
|
.left(
|
|
|
|
fill_start_delta_lens
|
|
|
|
.clone()
|
|
|
|
.map(|(start_t, _)| Percentage(start_t * 100.0)),
|
|
|
|
)
|
|
|
|
.width(fill_start_delta_lens.map(|(_, delta)| Percentage(delta * 100.0)))
|
|
|
|
// Hovering is handled on the param slider as a whole, this
|
|
|
|
// should not affect that
|
|
|
|
.hoverable(false);
|
|
|
|
|
|
|
|
// If the parameter is being modulated, then we'll display another
|
|
|
|
// filled bar showing the current modulation delta
|
|
|
|
// VIZIA's bindings make this a bit, uh, difficult to read
|
|
|
|
Element::new(cx)
|
|
|
|
.class("fill")
|
|
|
|
.class("fill--modulation")
|
|
|
|
.height(Stretch(1.0))
|
|
|
|
.visibility(
|
|
|
|
modulation_start_delta_lens
|
|
|
|
.clone()
|
|
|
|
.map(|(_, delta)| *delta != 0.0),
|
|
|
|
)
|
|
|
|
// Widths cannot be negative, so we need to compensate the start
|
|
|
|
// position if the width does happen to be negative
|
|
|
|
.width(
|
|
|
|
modulation_start_delta_lens
|
|
|
|
.clone()
|
|
|
|
.map(|(_, delta)| Percentage(delta.abs() * 100.0)),
|
|
|
|
)
|
|
|
|
.left(modulation_start_delta_lens.map(|(start_t, delta)| {
|
|
|
|
if *delta < 0.0 {
|
|
|
|
Percentage((start_t + delta) * 100.0)
|
|
|
|
} else {
|
|
|
|
Percentage(start_t * 100.0)
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
.hoverable(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create the text part of the slider. Shown on top of the fill using a `ZStack`.
|
|
|
|
fn slider_label_view<P: Param, L: Lens<Target = String>>(
|
|
|
|
cx: &mut Context,
|
|
|
|
param: &P,
|
|
|
|
style: ParamSliderStyle,
|
|
|
|
display_value_lens: impl Lens<Target = String>,
|
|
|
|
make_preview_value_lens: impl Fn(f32) -> L,
|
2022-11-08 01:08:50 +11:00
|
|
|
label_override_lens: impl Lens<Target = Option<String>>,
|
2022-11-07 23:35:48 +11:00
|
|
|
) {
|
|
|
|
let step_count = param.step_count();
|
|
|
|
|
|
|
|
// Either display the current value, or display all values over the
|
|
|
|
// parameter's steps
|
|
|
|
// TODO: Do the same thing as in the iced widget where we draw the
|
|
|
|
// text overlapping the fill area slightly differently. We can
|
|
|
|
// set the cip region directly in vizia.
|
|
|
|
match (style, step_count) {
|
|
|
|
(ParamSliderStyle::CurrentStepLabeled { .. }, Some(step_count)) => {
|
|
|
|
HStack::new(cx, |cx| {
|
|
|
|
// There are step_count + 1 possible values for a
|
|
|
|
// discrete parameter
|
|
|
|
for value in 0..step_count + 1 {
|
|
|
|
let normalized_value = value as f32 / step_count as f32;
|
|
|
|
let preview_lens = make_preview_value_lens(normalized_value);
|
|
|
|
|
|
|
|
Label::new(cx, preview_lens)
|
|
|
|
.class("value")
|
|
|
|
.class("value--multiple")
|
|
|
|
.child_space(Stretch(1.0))
|
|
|
|
.height(Stretch(1.0))
|
|
|
|
.width(Stretch(1.0))
|
|
|
|
.hoverable(false);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.height(Stretch(1.0))
|
|
|
|
.width(Stretch(1.0))
|
|
|
|
.hoverable(false);
|
|
|
|
}
|
|
|
|
_ => {
|
2022-11-08 01:08:50 +11:00
|
|
|
Binding::new(cx, label_override_lens, move |cx, label_override_lens| {
|
|
|
|
// If the label override is set then we'll use that. If not, the parameter's
|
|
|
|
// current display value (before modulation) is used.
|
|
|
|
match label_override_lens.get(cx) {
|
|
|
|
Some(label_override) => Label::new(cx, &label_override),
|
|
|
|
None => Label::new(cx, display_value_lens.clone()),
|
|
|
|
}
|
2022-11-07 23:35:48 +11:00
|
|
|
.class("value")
|
|
|
|
.class("value--single")
|
|
|
|
.child_space(Stretch(1.0))
|
|
|
|
.height(Stretch(1.0))
|
|
|
|
.width(Stretch(1.0))
|
|
|
|
.hoverable(false);
|
2022-11-08 01:08:50 +11:00
|
|
|
});
|
2022-11-07 23:35:48 +11:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculate the start position and width of the slider's fill region based on the selected
|
|
|
|
/// style, the parameter's current value, and the parameter's step sizes. The resulting tuple
|
|
|
|
/// `(start_t, delta)` corresponds to the start and the signed width of the bar. `start_t` is in
|
|
|
|
/// `[0, 1]`, and `delta` is in `[-1, 1]`.
|
|
|
|
fn compute_fill_start_delta<P: Param>(
|
|
|
|
style: ParamSliderStyle,
|
|
|
|
param: &P,
|
|
|
|
current_value: f32,
|
|
|
|
) -> (f32, f32) {
|
|
|
|
let default_value = param.default_normalized_value();
|
|
|
|
let step_count = param.step_count();
|
|
|
|
let draw_fill_from_default = matches!(style, ParamSliderStyle::Centered)
|
|
|
|
&& step_count.is_none()
|
|
|
|
&& (0.45..=0.55).contains(&default_value);
|
|
|
|
|
|
|
|
match style {
|
|
|
|
ParamSliderStyle::Centered if draw_fill_from_default => {
|
|
|
|
let delta = (default_value - current_value).abs();
|
|
|
|
|
|
|
|
// Don't draw the filled portion at all if it could have been a
|
|
|
|
// rounding error since those slivers just look weird
|
|
|
|
(
|
|
|
|
default_value.min(current_value),
|
|
|
|
if delta >= 1e-3 { delta } else { 0.0 },
|
|
|
|
)
|
|
|
|
}
|
|
|
|
ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => (0.0, current_value),
|
|
|
|
ParamSliderStyle::CurrentStep { even: true }
|
|
|
|
| ParamSliderStyle::CurrentStepLabeled { even: true }
|
|
|
|
if step_count.is_some() =>
|
|
|
|
{
|
|
|
|
// Assume the normalized value is distributed evenly
|
|
|
|
// across the range.
|
|
|
|
let step_count = step_count.unwrap() as f32;
|
|
|
|
let discrete_values = step_count + 1.0;
|
|
|
|
let previous_step = (current_value * step_count) / discrete_values;
|
|
|
|
|
|
|
|
(previous_step, discrete_values.recip())
|
|
|
|
}
|
|
|
|
ParamSliderStyle::CurrentStep { .. } | ParamSliderStyle::CurrentStepLabeled { .. } => {
|
2022-11-09 08:03:26 +11:00
|
|
|
let previous_step = param.previous_normalized_step(current_value, false);
|
|
|
|
let next_step = param.next_normalized_step(current_value, false);
|
2022-11-07 23:35:48 +11:00
|
|
|
|
|
|
|
(
|
|
|
|
(previous_step + current_value) / 2.0,
|
|
|
|
((next_step - current_value) + (current_value - previous_step)) / 2.0,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The same as `compute_fill_start_delta`, but just showing the modulation offset.
|
|
|
|
fn compute_modulation_fill_start_delta<P: Param>(
|
|
|
|
style: ParamSliderStyle,
|
|
|
|
param: &P,
|
|
|
|
) -> (f32, f32) {
|
|
|
|
match style {
|
|
|
|
// Don't show modulation for stepped parameters since it wouldn't
|
|
|
|
// make a lot of sense visually
|
|
|
|
ParamSliderStyle::CurrentStep { .. } | ParamSliderStyle::CurrentStepLabeled { .. } => {
|
|
|
|
(0.0, 0.0)
|
|
|
|
}
|
|
|
|
ParamSliderStyle::Centered | ParamSliderStyle::FromLeft => {
|
|
|
|
let modulation_start = param.unmodulated_normalized_value();
|
|
|
|
|
|
|
|
(
|
|
|
|
modulation_start,
|
|
|
|
param.modulated_normalized_value() - modulation_start,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-07 05:13:08 +11:00
|
|
|
/// `self.param_base.set_normalized_value()`, but resulting from a mouse drag. When using the
|
|
|
|
/// 'even' stepped slider styles from [`ParamSliderStyle`] this will remap the normalized range
|
|
|
|
/// to match up with the fill value display. This still needs to be wrapped in a parameter
|
|
|
|
/// automation gesture.
|
2022-09-23 03:52:11 +10:00
|
|
|
fn set_normalized_value_drag(&self, cx: &mut EventContext, normalized_value: f32) {
|
2022-11-07 05:13:08 +11:00
|
|
|
let normalized_value = match (self.style, self.param_base.step_count()) {
|
2022-03-23 09:28:15 +11:00
|
|
|
(
|
2022-09-23 03:52:11 +10:00
|
|
|
ParamSliderStyle::CurrentStep { even: true }
|
|
|
|
| ParamSliderStyle::CurrentStepLabeled { even: true },
|
2022-03-23 09:28:15 +11:00
|
|
|
Some(step_count),
|
|
|
|
) => {
|
|
|
|
// We'll remap the value range to be the same as the displayed range, e.g. with each
|
|
|
|
// value occupying an equal area on the slider instead of the centers of those
|
|
|
|
// ranges being distributed over the entire `[0, 1]` range.
|
|
|
|
let discrete_values = step_count as f32 + 1.0;
|
|
|
|
let rounded_value = ((normalized_value * discrete_values) - 0.5).round();
|
|
|
|
rounded_value / step_count as f32
|
|
|
|
}
|
|
|
|
_ => normalized_value,
|
|
|
|
};
|
|
|
|
|
2022-11-07 05:13:08 +11:00
|
|
|
self.param_base.set_normalized_value(cx, normalized_value);
|
2022-03-23 09:28:15 +11:00
|
|
|
}
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
impl View for ParamSlider {
|
2022-06-18 09:59:53 +10:00
|
|
|
fn element(&self) -> Option<&'static str> {
|
|
|
|
Some("param-slider")
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
|
|
|
|
2022-09-23 03:52:11 +10:00
|
|
|
fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
|
2022-10-09 00:48:18 +11:00
|
|
|
event.map(|param_slider_event, meta| match param_slider_event {
|
2022-04-25 02:12:06 +10:00
|
|
|
ParamSliderEvent::CancelTextInput => {
|
2022-09-22 02:30:02 +10:00
|
|
|
self.text_input_active = false;
|
2022-06-18 09:59:53 +10:00
|
|
|
cx.set_active(false);
|
2022-10-09 00:48:18 +11:00
|
|
|
|
|
|
|
meta.consume();
|
2022-04-25 02:12:06 +10:00
|
|
|
}
|
|
|
|
ParamSliderEvent::TextInput(string) => {
|
2022-11-07 05:13:08 +11:00
|
|
|
if let Some(normalized_value) = self.param_base.string_to_normalized_value(string) {
|
|
|
|
self.param_base.begin_set_parameter(cx);
|
|
|
|
self.param_base.set_normalized_value(cx, normalized_value);
|
|
|
|
self.param_base.end_set_parameter(cx);
|
2022-03-20 01:28:58 +11:00
|
|
|
}
|
|
|
|
|
2022-09-22 02:30:02 +10:00
|
|
|
self.text_input_active = false;
|
2022-10-09 00:48:18 +11:00
|
|
|
|
|
|
|
meta.consume();
|
2022-03-20 01:28:58 +11:00
|
|
|
}
|
2022-04-25 02:12:06 +10:00
|
|
|
});
|
|
|
|
|
2022-10-09 00:48:18 +11:00
|
|
|
event.map(|window_event, meta| match window_event {
|
2022-10-09 00:52:36 +11:00
|
|
|
// Vizia always captures the third mouse click as a triple click. Treating that triple
|
|
|
|
// click as a regular mouse button makes double click followed by another drag work as
|
|
|
|
// expected, instead of requiring a delay or an additional click. Double double click
|
|
|
|
// still won't work.
|
2022-11-07 05:45:34 +11:00
|
|
|
WindowEvent::MouseDown(MouseButton::Left)
|
2022-10-09 00:52:36 +11:00
|
|
|
| WindowEvent::MouseTripleClick(MouseButton::Left) => {
|
2022-09-23 03:52:11 +10:00
|
|
|
if cx.modifiers.alt() {
|
2022-04-25 02:12:06 +10:00
|
|
|
// ALt+Click brings up a text entry dialog
|
2022-09-22 02:30:02 +10:00
|
|
|
self.text_input_active = true;
|
2022-06-18 09:59:53 +10:00
|
|
|
cx.set_active(true);
|
2022-09-23 04:07:04 +10:00
|
|
|
} else if cx.modifiers.command() {
|
2022-04-25 02:12:06 +10:00
|
|
|
// Ctrl+Click and double click should reset the parameter instead of initiating
|
|
|
|
// a drag operation
|
2022-11-07 05:45:34 +11:00
|
|
|
self.param_base.begin_set_parameter(cx);
|
|
|
|
self.param_base
|
|
|
|
.set_normalized_value(cx, self.param_base.default_normalized_value());
|
|
|
|
self.param_base.end_set_parameter(cx);
|
2022-04-25 02:12:06 +10:00
|
|
|
} else {
|
|
|
|
self.drag_active = true;
|
|
|
|
cx.capture();
|
|
|
|
// NOTE: Otherwise we don't get key up events
|
2022-06-18 09:59:53 +10:00
|
|
|
cx.focus();
|
|
|
|
cx.set_active(true);
|
2022-04-25 02:12:06 +10:00
|
|
|
|
|
|
|
// When holding down shift while clicking on a parameter we want to granuarly
|
|
|
|
// edit the parameter without jumping to a new value
|
2022-11-07 05:13:08 +11:00
|
|
|
self.param_base.begin_set_parameter(cx);
|
2022-09-23 03:52:11 +10:00
|
|
|
if cx.modifiers.shift() {
|
2022-11-07 05:45:34 +11:00
|
|
|
self.granular_drag_start_x_value = Some((
|
|
|
|
cx.mouse.cursorx,
|
|
|
|
self.param_base.unmodulated_normalized_value(),
|
|
|
|
));
|
2022-03-19 11:17:13 +11:00
|
|
|
} else {
|
2022-04-25 02:12:06 +10:00
|
|
|
self.granular_drag_start_x_value = None;
|
|
|
|
self.set_normalized_value_drag(
|
|
|
|
cx,
|
2022-09-23 03:52:11 +10:00
|
|
|
util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx),
|
2022-04-25 02:12:06 +10:00
|
|
|
);
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
|
|
|
}
|
2022-10-09 00:48:18 +11:00
|
|
|
|
|
|
|
meta.consume();
|
2022-04-25 02:12:06 +10:00
|
|
|
}
|
|
|
|
WindowEvent::MouseDoubleClick(MouseButton::Left) => {
|
2022-09-23 04:07:04 +10:00
|
|
|
// Ctrl+Click and double click should reset the parameter instead of initiating
|
|
|
|
// a drag operation
|
2022-11-07 05:13:08 +11:00
|
|
|
self.param_base.begin_set_parameter(cx);
|
2022-11-07 05:45:34 +11:00
|
|
|
self.param_base
|
|
|
|
.set_normalized_value(cx, self.param_base.default_normalized_value());
|
2022-11-07 05:13:08 +11:00
|
|
|
self.param_base.end_set_parameter(cx);
|
2022-10-09 00:48:18 +11:00
|
|
|
|
|
|
|
meta.consume();
|
2022-04-25 02:12:06 +10:00
|
|
|
}
|
|
|
|
WindowEvent::MouseUp(MouseButton::Left) => {
|
|
|
|
if self.drag_active {
|
|
|
|
self.drag_active = false;
|
|
|
|
cx.release();
|
2022-06-18 09:59:53 +10:00
|
|
|
cx.set_active(false);
|
2022-03-19 11:17:13 +11:00
|
|
|
|
2022-11-07 05:13:08 +11:00
|
|
|
self.param_base.end_set_parameter(cx);
|
2022-10-09 00:48:18 +11:00
|
|
|
|
|
|
|
meta.consume();
|
2022-04-25 02:12:06 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
WindowEvent::MouseMove(x, _y) => {
|
|
|
|
if self.drag_active {
|
|
|
|
// If shift is being held then the drag should be more granular instead of
|
|
|
|
// absolute
|
2022-09-23 03:52:11 +10:00
|
|
|
if cx.modifiers.shift() {
|
2022-04-25 02:12:06 +10:00
|
|
|
let (drag_start_x, drag_start_value) =
|
|
|
|
*self.granular_drag_start_x_value.get_or_insert_with(|| {
|
2022-11-07 05:45:34 +11:00
|
|
|
(
|
|
|
|
cx.mouse.cursorx,
|
|
|
|
self.param_base.unmodulated_normalized_value(),
|
|
|
|
)
|
2022-04-25 02:12:06 +10:00
|
|
|
});
|
2022-03-19 11:17:13 +11:00
|
|
|
|
2022-04-25 02:12:06 +10:00
|
|
|
self.set_normalized_value_drag(
|
|
|
|
cx,
|
|
|
|
util::remap_current_entity_x_coordinate(
|
2022-03-19 11:17:13 +11:00
|
|
|
cx,
|
2022-04-25 02:12:06 +10:00
|
|
|
// This can be optimized a bit
|
|
|
|
util::remap_current_entity_x_t(cx, drag_start_value)
|
|
|
|
+ (*x - drag_start_x) * GRANULAR_DRAG_MULTIPLIER,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
2022-03-19 12:18:30 +11:00
|
|
|
self.granular_drag_start_x_value = None;
|
2022-04-25 02:12:06 +10:00
|
|
|
|
|
|
|
self.set_normalized_value_drag(
|
2022-03-19 11:48:57 +11:00
|
|
|
cx,
|
2022-04-25 02:12:06 +10:00
|
|
|
util::remap_current_entity_x_coordinate(cx, *x),
|
2022-03-19 11:48:57 +11:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
2022-04-25 02:12:06 +10:00
|
|
|
WindowEvent::KeyUp(_, Some(Key::Shift)) => {
|
|
|
|
// If this happens while dragging, snap back to reality uh I mean the current screen
|
|
|
|
// position
|
|
|
|
if self.drag_active && self.granular_drag_start_x_value.is_some() {
|
|
|
|
self.granular_drag_start_x_value = None;
|
2022-11-07 05:45:34 +11:00
|
|
|
self.param_base.set_normalized_value(
|
|
|
|
cx,
|
|
|
|
util::remap_current_entity_x_coordinate(cx, cx.mouse.cursorx),
|
|
|
|
);
|
2022-04-25 02:12:06 +10:00
|
|
|
}
|
|
|
|
}
|
2022-11-09 07:20:40 +11:00
|
|
|
WindowEvent::MouseScroll(_scroll_x, scroll_y) if self.use_scroll_wheel => {
|
|
|
|
// With a regular scroll wheel `scroll_y` will only ever be -1 or 1, but with smooth
|
|
|
|
// scrolling trackpads being a thing `scroll_y` could be anything.
|
|
|
|
self.scrolled_lines += scroll_y;
|
|
|
|
|
|
|
|
if self.scrolled_lines.abs() >= 1.0 {
|
2022-11-09 08:04:21 +11:00
|
|
|
let use_finer_steps = cx.modifiers.shift();
|
|
|
|
|
2022-11-09 07:20:40 +11:00
|
|
|
// Scrolling while dragging needs to be taken into account here
|
|
|
|
if !self.drag_active {
|
|
|
|
self.param_base.begin_set_parameter(cx);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut current_value = self.param_base.unmodulated_normalized_value();
|
|
|
|
|
|
|
|
while self.scrolled_lines >= 1.0 {
|
2022-11-09 08:04:21 +11:00
|
|
|
current_value = self
|
|
|
|
.param_base
|
|
|
|
.next_normalized_step(current_value, use_finer_steps);
|
2022-11-09 07:20:40 +11:00
|
|
|
self.param_base.set_normalized_value(cx, current_value);
|
|
|
|
self.scrolled_lines -= 1.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
while self.scrolled_lines <= -1.0 {
|
2022-11-09 08:03:26 +11:00
|
|
|
current_value = self
|
|
|
|
.param_base
|
2022-11-09 08:04:21 +11:00
|
|
|
.previous_normalized_step(current_value, use_finer_steps);
|
2022-11-09 07:20:40 +11:00
|
|
|
self.param_base.set_normalized_value(cx, current_value);
|
|
|
|
self.scrolled_lines += 1.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if !self.drag_active {
|
|
|
|
self.param_base.end_set_parameter(cx);
|
|
|
|
}
|
|
|
|
}
|
2022-11-09 07:41:29 +11:00
|
|
|
|
|
|
|
meta.consume();
|
2022-11-09 07:20:40 +11:00
|
|
|
}
|
2022-04-25 02:12:06 +10:00
|
|
|
_ => {}
|
|
|
|
});
|
2022-03-19 11:17:13 +11:00
|
|
|
}
|
|
|
|
}
|
2022-03-20 04:49:49 +11:00
|
|
|
|
|
|
|
/// Extension methods for [`ParamSlider`] handles.
|
|
|
|
pub trait ParamSliderExt {
|
2022-11-09 07:20:40 +11:00
|
|
|
/// Don't respond to scroll wheel events. Useful when this slider is used as part of a scrolling
|
|
|
|
/// view.
|
|
|
|
fn disable_scroll_wheel(self) -> Self;
|
|
|
|
|
2022-03-20 04:49:49 +11:00
|
|
|
/// Change how the [`ParamSlider`] visualizes the current value.
|
|
|
|
fn set_style(self, style: ParamSliderStyle) -> Self;
|
2022-11-08 01:08:50 +11:00
|
|
|
|
|
|
|
/// Manually set a fixed label for the slider instead of displaying the current value. This is
|
|
|
|
/// currently not reactive.
|
|
|
|
fn with_label(self, value: impl Into<String>) -> Self;
|
2022-03-20 04:49:49 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ParamSliderExt for Handle<'_, ParamSlider> {
|
2022-11-09 07:20:40 +11:00
|
|
|
fn disable_scroll_wheel(self) -> Self {
|
|
|
|
self.modify(|param_slider: &mut ParamSlider| param_slider.use_scroll_wheel = false)
|
|
|
|
}
|
|
|
|
|
2022-03-20 04:49:49 +11:00
|
|
|
fn set_style(self, style: ParamSliderStyle) -> Self {
|
2022-09-22 02:30:02 +10:00
|
|
|
self.modify(|param_slider: &mut ParamSlider| param_slider.style = style)
|
2022-03-20 04:49:49 +11:00
|
|
|
}
|
2022-11-08 01:08:50 +11:00
|
|
|
|
|
|
|
fn with_label(self, value: impl Into<String>) -> Self {
|
|
|
|
self.modify(|param_slider: &mut ParamSlider| {
|
|
|
|
param_slider.label_override = Some(value.into())
|
|
|
|
})
|
|
|
|
}
|
2022-03-20 04:49:49 +11:00
|
|
|
}
|