Allow text entry on Alt+Click
This commit is contained in:
parent
aa03b1d1f7
commit
efa1a5a0b4
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1942,6 +1942,7 @@ dependencies = [
|
||||||
name = "nih_plug_iced"
|
name = "nih_plug_iced"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic_refcell",
|
||||||
"baseview",
|
"baseview",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"iced_baseview",
|
"iced_baseview",
|
||||||
|
|
|
@ -56,6 +56,7 @@ smol = ["iced_baseview/smol"]
|
||||||
nih_plug = { path = ".." }
|
nih_plug = { path = ".." }
|
||||||
nih_plug_assets = { git = "https://github.com/robbert-vdh/nih_plug_assets.git" }
|
nih_plug_assets = { git = "https://github.com/robbert-vdh/nih_plug_assets.git" }
|
||||||
|
|
||||||
|
atomic_refcell = "0.1"
|
||||||
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"
|
||||||
# Upstream doesn't work with the current iced version, this branch also contains
|
# Upstream doesn't work with the current iced version, this branch also contains
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
//! A slider that integrates with NIH-plug's [`Param`] types.
|
//! A slider that integrates with NIH-plug's [`Param`] types.
|
||||||
|
|
||||||
|
use atomic_refcell::AtomicRefCell;
|
||||||
|
use nih_plug::prelude::{GuiContext, Param, ParamSetter};
|
||||||
|
|
||||||
|
use crate::backend::widget;
|
||||||
|
use crate::backend::Renderer;
|
||||||
|
use crate::renderer::Renderer as GraphicsRenderer;
|
||||||
|
use crate::text::Renderer as TextRenderer;
|
||||||
use crate::{
|
use crate::{
|
||||||
alignment, event, keyboard, layout, mouse, renderer, text, touch, Clipboard, Color, Element,
|
alignment, event, keyboard, layout, mouse, renderer, text, touch, Clipboard, Color, Element,
|
||||||
Event, Font, Layout, Length, Point, Rectangle, Shell, Size, Widget,
|
Event, Font, Layout, Length, Point, Rectangle, Shell, Size, TextInput, Vector, Widget,
|
||||||
};
|
};
|
||||||
use iced_baseview::backend::Renderer;
|
|
||||||
use iced_baseview::renderer::Renderer as GraphicsRenderer;
|
|
||||||
use iced_baseview::text::Renderer as TextRenderer;
|
|
||||||
use nih_plug::prelude::{GuiContext, Param, ParamSetter};
|
|
||||||
|
|
||||||
use super::util;
|
use super::util;
|
||||||
use super::ParamMessage;
|
use super::ParamMessage;
|
||||||
|
@ -16,10 +19,12 @@ use super::ParamMessage;
|
||||||
/// noramlized parameter.
|
/// noramlized parameter.
|
||||||
const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1;
|
const GRANULAR_DRAG_MULTIPLIER: f32 = 0.1;
|
||||||
|
|
||||||
|
/// The thickness of this widget's borders.
|
||||||
|
const BORDER_WIDTH: f32 = 1.0;
|
||||||
|
|
||||||
/// A slider that integrates with NIH-plug's [`Param`] types.
|
/// A slider that integrates with NIH-plug's [`Param`] types.
|
||||||
///
|
///
|
||||||
/// TODO: There are currently no styling options at all
|
/// TODO: There are currently no styling options at all
|
||||||
/// TODO: Handle Alt+click for text entry
|
|
||||||
/// TODO: Handle scrolling for steps (and shift+scroll for smaller steps?)
|
/// TODO: Handle scrolling for steps (and shift+scroll for smaller steps?)
|
||||||
pub struct ParamSlider<'a, P: Param> {
|
pub struct ParamSlider<'a, P: Param> {
|
||||||
state: &'a mut State,
|
state: &'a mut State,
|
||||||
|
@ -48,6 +53,46 @@ pub struct State {
|
||||||
/// Will be set to `true` if we just reset the parameter since you could otherwise reset the
|
/// Will be set to `true` if we just reset the parameter since you could otherwise reset the
|
||||||
/// parameter and then move your mouse around to still set it a non-default value.
|
/// parameter and then move your mouse around to still set it a non-default value.
|
||||||
ignore_changes: bool,
|
ignore_changes: bool,
|
||||||
|
|
||||||
|
/// State for the text input overlay that will be shown when this widget is alt+clicked.
|
||||||
|
text_input_state: AtomicRefCell<widget::text_input::State>,
|
||||||
|
/// The text that's currently in the text input. If this is set to `None`, then the text input
|
||||||
|
/// is not visible.
|
||||||
|
text_input_value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An internal message for intercep- I mean handling output from the embedded [`TextInpu`] widget.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum TextInputMessage {
|
||||||
|
/// A new value was entered in the text input dialog.
|
||||||
|
Value(String),
|
||||||
|
/// Enter was pressed.
|
||||||
|
Submit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default text input style with the border removed.
|
||||||
|
struct TextInputStyle;
|
||||||
|
|
||||||
|
impl widget::text_input::StyleSheet for TextInputStyle {
|
||||||
|
fn active(&self) -> widget::text_input::Style {
|
||||||
|
widget::text_input::Style::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focused(&self) -> widget::text_input::Style {
|
||||||
|
widget::text_input::Style::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_color(&self) -> Color {
|
||||||
|
Color::from_rgb(0.7, 0.7, 0.7)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_color(&self) -> Color {
|
||||||
|
Color::from_rgb(0.3, 0.3, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection_color(&self) -> Color {
|
||||||
|
Color::from_rgb(0.8, 0.8, 1.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, P: Param> ParamSlider<'a, P> {
|
impl<'a, P: Param> ParamSlider<'a, P> {
|
||||||
|
@ -114,12 +159,69 @@ impl<'a, P: Param> Widget<ParamMessage, Renderer> for ParamSlider<'a, P> {
|
||||||
event: Event,
|
event: Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor_position: Point,
|
cursor_position: Point,
|
||||||
_renderer: &Renderer,
|
renderer: &Renderer,
|
||||||
_clipboard: &mut dyn Clipboard,
|
clipboard: &mut dyn Clipboard,
|
||||||
shell: &mut Shell<'_, ParamMessage>,
|
shell: &mut Shell<'_, ParamMessage>,
|
||||||
) -> event::Status {
|
) -> event::Status {
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
|
// The pressence of a value in `self.state.text_input_value` indicates that the field should
|
||||||
|
// be focussed. The field handles defocussing by itself
|
||||||
|
// FIMXE: This is super hacky, I have no idea how you can reuse the text input widget
|
||||||
|
// otherwise. Widgets are not supposed to handle messages from other widgets, but
|
||||||
|
// we'll do so anyways by using a special `TextInputMessage` type and our own
|
||||||
|
// `Shell`.
|
||||||
|
let text_input_status = if let Some(current_value) = &self.state.text_input_value {
|
||||||
|
let event = event.clone();
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
let mut shell = Shell::new(&mut messages);
|
||||||
|
let status = self.with_text_input(layout, current_value, |mut text_input, layout| {
|
||||||
|
text_input.on_event(
|
||||||
|
event,
|
||||||
|
layout,
|
||||||
|
cursor_position,
|
||||||
|
renderer,
|
||||||
|
clipboard,
|
||||||
|
&mut shell,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pressing escape will unfocus the text field, so we should propagate that change in
|
||||||
|
// our own model
|
||||||
|
if self.state.text_input_state.borrow().is_focused() {
|
||||||
|
for message in messages {
|
||||||
|
match message {
|
||||||
|
TextInputMessage::Value(s) => self.state.text_input_value = Some(s),
|
||||||
|
TextInputMessage::Submit => {
|
||||||
|
if let Some(normalized_value) = self
|
||||||
|
.state
|
||||||
|
.text_input_value
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| self.param.string_to_normalized_value(s))
|
||||||
|
{
|
||||||
|
self.setter.begin_set_parameter(self.param);
|
||||||
|
self.setter
|
||||||
|
.set_parameter_normalized(self.param, normalized_value);
|
||||||
|
self.setter.end_set_parameter(self.param);
|
||||||
|
}
|
||||||
|
|
||||||
|
// And defocus the text input widget again
|
||||||
|
self.state.text_input_value = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.state.text_input_value = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
status
|
||||||
|
} else {
|
||||||
|
event::Status::Ignored
|
||||||
|
};
|
||||||
|
if text_input_status == event::Status::Captured {
|
||||||
|
return event::Status::Captured;
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||||
|
@ -130,7 +232,17 @@ impl<'a, P: Param> Widget<ParamMessage, Renderer> for ParamSlider<'a, P> {
|
||||||
|
|
||||||
let click = mouse::Click::new(cursor_position, self.state.last_click);
|
let click = mouse::Click::new(cursor_position, self.state.last_click);
|
||||||
self.state.last_click = Some(click);
|
self.state.last_click = Some(click);
|
||||||
if self.state.keyboard_modifiers.command()
|
if self.state.keyboard_modifiers.alt() {
|
||||||
|
// Alt+click should not start a drag, instead it should show the text entry
|
||||||
|
// widget
|
||||||
|
self.state.drag_active = false;
|
||||||
|
|
||||||
|
self.state.text_input_value = Some(self.param.to_string());
|
||||||
|
self.state
|
||||||
|
.text_input_state
|
||||||
|
.borrow_mut()
|
||||||
|
.move_cursor_to_end();
|
||||||
|
} else if self.state.keyboard_modifiers.command()
|
||||||
|| matches!(click.kind(), mouse::click::Kind::Double)
|
|| matches!(click.kind(), mouse::click::Kind::Double)
|
||||||
{
|
{
|
||||||
// Immediately trigger a parameter update if the value would be different, or
|
// Immediately trigger a parameter update if the value would be different, or
|
||||||
|
@ -253,8 +365,6 @@ impl<'a, P: Param> Widget<ParamMessage, Renderer> for ParamSlider<'a, P> {
|
||||||
cursor_position: Point,
|
cursor_position: Point,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
const BORDER_WIDTH: f32 = 1.0;
|
|
||||||
|
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
let is_mouse_over = bounds.contains(cursor_position);
|
let is_mouse_over = bounds.contains(cursor_position);
|
||||||
|
|
||||||
|
@ -300,39 +410,45 @@ impl<'a, P: Param> Widget<ParamMessage, Renderer> for ParamSlider<'a, P> {
|
||||||
fill_color,
|
fill_color,
|
||||||
);
|
);
|
||||||
|
|
||||||
// We'll overlay the label on the slider. To make it more readable (and because it looks
|
// Only draw the text input widget when it gets focussed. Otherwise, overlay the label with
|
||||||
// cool), the parts that overlap with the fill rect will be rendered in white while the rest
|
// the slider. To make it more readable (and because it looks cool), the parts that overlap
|
||||||
// will be rendered in black.
|
// with the fill rect will be rendered in white while the rest will be rendered in black.
|
||||||
let display_value = self.param.to_string();
|
if let Some(current_value) = &self.state.text_input_value {
|
||||||
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()) as f32;
|
self.with_text_input(layout, current_value, |text_input, layout| {
|
||||||
let text_bounds = Rectangle {
|
text_input.draw(renderer, layout, cursor_position, None)
|
||||||
x: bounds.center_x(),
|
})
|
||||||
y: bounds.center_y(),
|
} else {
|
||||||
..bounds
|
let display_value = self.param.to_string();
|
||||||
};
|
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()) as f32;
|
||||||
renderer.fill_text(text::Text {
|
let text_bounds = Rectangle {
|
||||||
content: &display_value,
|
x: bounds.center_x(),
|
||||||
font: self.font,
|
y: bounds.center_y(),
|
||||||
size: text_size,
|
..bounds
|
||||||
bounds: text_bounds,
|
};
|
||||||
color: style.text_color,
|
|
||||||
horizontal_alignment: alignment::Horizontal::Center,
|
|
||||||
vertical_alignment: alignment::Vertical::Center,
|
|
||||||
});
|
|
||||||
|
|
||||||
// This will clip to the filled area
|
|
||||||
renderer.with_layer(fill_rect, |renderer| {
|
|
||||||
let filled_text_color = Color::from_rgb8(80, 80, 80);
|
|
||||||
renderer.fill_text(text::Text {
|
renderer.fill_text(text::Text {
|
||||||
content: &display_value,
|
content: &display_value,
|
||||||
font: self.font,
|
font: self.font,
|
||||||
size: text_size,
|
size: text_size,
|
||||||
bounds: text_bounds,
|
bounds: text_bounds,
|
||||||
color: filled_text_color,
|
color: style.text_color,
|
||||||
horizontal_alignment: alignment::Horizontal::Center,
|
horizontal_alignment: alignment::Horizontal::Center,
|
||||||
vertical_alignment: alignment::Vertical::Center,
|
vertical_alignment: alignment::Vertical::Center,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// This will clip to the filled area
|
||||||
|
renderer.with_layer(fill_rect, |renderer| {
|
||||||
|
let filled_text_color = Color::from_rgb8(80, 80, 80);
|
||||||
|
renderer.fill_text(text::Text {
|
||||||
|
content: &display_value,
|
||||||
|
font: self.font,
|
||||||
|
size: text_size,
|
||||||
|
bounds: text_bounds,
|
||||||
|
color: filled_text_color,
|
||||||
|
horizontal_alignment: alignment::Horizontal::Center,
|
||||||
|
vertical_alignment: alignment::Vertical::Center,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,6 +465,46 @@ impl<'a, P: Param> ParamSlider<'a, P> {
|
||||||
Element::from(self).map(f)
|
Element::from(self).map(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a temporary [`TextInput`] hooked up to [`State::text_input_value`] and outputting
|
||||||
|
/// [`TextInputMessage`] messages and do something with it. This can be used to
|
||||||
|
fn with_text_input<R, F: FnOnce(TextInput<'_, TextInputMessage>, Layout) -> R>(
|
||||||
|
&self,
|
||||||
|
layout: Layout,
|
||||||
|
current_value: &str,
|
||||||
|
f: F,
|
||||||
|
) -> R {
|
||||||
|
let mut text_input_state = self.state.text_input_state.borrow_mut();
|
||||||
|
text_input_state.focus();
|
||||||
|
|
||||||
|
let text_input = TextInput::new(
|
||||||
|
&mut text_input_state,
|
||||||
|
"",
|
||||||
|
current_value,
|
||||||
|
TextInputMessage::Value,
|
||||||
|
)
|
||||||
|
.width(self.width)
|
||||||
|
.style(TextInputStyle)
|
||||||
|
.on_submit(TextInputMessage::Submit);
|
||||||
|
|
||||||
|
// Make sure to not draw over the borders
|
||||||
|
let offset_node = layout::Node::with_children(
|
||||||
|
Size {
|
||||||
|
width: layout.bounds().size().width - (BORDER_WIDTH * 2.0),
|
||||||
|
height: layout.bounds().size().height - (BORDER_WIDTH * 2.0),
|
||||||
|
},
|
||||||
|
vec![layout::Node::new(layout.bounds().size())],
|
||||||
|
);
|
||||||
|
let offset_layout = Layout::with_offset(
|
||||||
|
Vector {
|
||||||
|
x: layout.position().x + BORDER_WIDTH,
|
||||||
|
y: layout.position().y + BORDER_WIDTH,
|
||||||
|
},
|
||||||
|
&offset_node,
|
||||||
|
);
|
||||||
|
|
||||||
|
f(text_input, offset_layout)
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the normalized value for a parameter if that would change the parameter's plain value
|
/// Set the normalized value for a parameter if that would change the parameter's plain value
|
||||||
/// (to avoid unnecessary duplicate parameter changes). The begin- and end set parameter
|
/// (to avoid unnecessary duplicate parameter changes). The begin- and end set parameter
|
||||||
/// messages need to be sent before calling this function.
|
/// messages need to be sent before calling this function.
|
||||||
|
|
Loading…
Reference in a new issue