mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 01:08:11 +08:00
Add a range slider mode to the spectrum widget and slider rows to node properties (#4531)
* Add a range slider mode to the spectrum widget and slider rows to node properties * Reset the weighted strength slider to the node's default of zero and skip markers off the track when picking up the nearest one
This commit is contained in:
@@ -662,7 +662,7 @@ pub struct SpectrumInput {
|
|||||||
/// Whether to render midpoint diamonds between adjacent markers (only meaningful for gradient-like uses).
|
/// Whether to render midpoint diamonds between adjacent markers (only meaningful for gradient-like uses).
|
||||||
#[serde(rename = "showMidpoints")]
|
#[serde(rename = "showMidpoints")]
|
||||||
pub show_midpoints: bool,
|
pub show_midpoints: bool,
|
||||||
/// Whether clicking the track inserts a new marker at the click position.
|
/// Whether clicking the track inserts a new marker at the click position. Otherwise the click picks up the nearest marker.
|
||||||
#[serde(rename = "allowInsert")]
|
#[serde(rename = "allowInsert")]
|
||||||
pub allow_insert: bool,
|
pub allow_insert: bool,
|
||||||
/// Whether right-click or pressing Delete removes a marker. The handler still has the final say on whether the deletion goes through (e.g., enforcing a minimum count).
|
/// Whether right-click or pressing Delete removes a marker. The handler still has the final say on whether the deletion goes through (e.g., enforcing a minimum count).
|
||||||
@@ -673,6 +673,9 @@ pub struct SpectrumInput {
|
|||||||
pub allow_reorder: bool,
|
pub allow_reorder: bool,
|
||||||
/// Compact mode: 8px track height with 8px top padding, for use in rows alongside other widgets.
|
/// Compact mode: 8px track height with 8px top padding, for use in rows alongside other widgets.
|
||||||
pub narrow: bool,
|
pub narrow: bool,
|
||||||
|
/// Plain range-slider mode, for a number beside its number input: a flat 4px track is drawn in place of the gradient, so `track` is never shown.
|
||||||
|
#[serde(rename = "rangeSlider")]
|
||||||
|
pub range_slider: bool,
|
||||||
/// Whether the input is disabled (dimmed and read-only).
|
/// Whether the input is disabled (dimmed and read-only).
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
mod document_node_derive;
|
mod document_node_derive;
|
||||||
|
|
||||||
use super::node_properties::choice::enum_choice;
|
use super::node_properties::choice::enum_choice;
|
||||||
use super::node_properties::{self, ParameterWidgetsInfo};
|
use super::node_properties::{self, ParameterWidgetsInfo, SliderRange};
|
||||||
use super::utility_types::{FrontendNodeType, InputTypeConstraint};
|
use super::utility_types::{FrontendNodeType, InputTypeConstraint};
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{
|
use crate::messages::portfolio::document::utility_types::network_interface::{
|
||||||
@@ -1073,19 +1073,26 @@ fn static_input_properties() -> InputProperties {
|
|||||||
ParameterWidgetsInfo::at_index(node_id, index, false, context),
|
ParameterWidgetsInfo::at_index(node_id, index, false, context),
|
||||||
index - 1,
|
index - 1,
|
||||||
number_input,
|
number_input,
|
||||||
|
None,
|
||||||
))])
|
))])
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
map.insert(
|
map.insert(
|
||||||
// Like `optional_f64`, but the number input is configured as a percentage with a 0-100 range.
|
// Like `optional_f64`, but with a 0-100% range slider beside the number input, double-click restoring the full 100%.
|
||||||
// As with `optional_f64`, the bool input must be at the input index directly before the f64 input.
|
// As with `optional_f64`, the bool input must be at the input index directly before the f64 input.
|
||||||
"optional_percentage".to_string(),
|
"optional_percentage".to_string(),
|
||||||
Box::new(|node_id, index, context| {
|
Box::new(|node_id, index, context| {
|
||||||
let number_input = NumberInput::default().percentage().min(0.).max(100.);
|
let number_input = NumberInput::default().mode_increment().unit("%").min(0.).max(100.);
|
||||||
|
let slider = SliderRange {
|
||||||
|
min: 0.,
|
||||||
|
max: 100.,
|
||||||
|
default: Some(100.),
|
||||||
|
};
|
||||||
Ok(vec![LayoutGroup::row(node_properties::optional_f64_widget(
|
Ok(vec![LayoutGroup::row(node_properties::optional_f64_widget(
|
||||||
ParameterWidgetsInfo::at_index(node_id, index, false, context),
|
ParameterWidgetsInfo::at_index(node_id, index, false, context),
|
||||||
index - 1,
|
index - 1,
|
||||||
number_input,
|
number_input,
|
||||||
|
Some(slider),
|
||||||
))])
|
))])
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -1241,13 +1248,13 @@ fn static_input_properties() -> InputProperties {
|
|||||||
"noise_properties_fractal_weighted_strength".to_string(),
|
"noise_properties_fractal_weighted_strength".to_string(),
|
||||||
Box::new(|node_id, index, context| {
|
Box::new(|node_id, index, context| {
|
||||||
let (fractal_active, coherent_noise_active, _, _, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?;
|
let (fractal_active, coherent_noise_active, _, _, _, domain_warp_only_fractal_type_wrongly_active) = node_properties::query_noise_pattern_state(node_id, context)?;
|
||||||
let fractal_weighted_strength = node_properties::number_widget(
|
let fractal_weighted_strength = node_properties::range_slider_widget(
|
||||||
ParameterWidgetsInfo::at_index(node_id, index, true, context),
|
ParameterWidgetsInfo::at_index(node_id, index, true, context),
|
||||||
NumberInput::default()
|
NumberInput::default()
|
||||||
.mode_range()
|
|
||||||
.min(0.)
|
.min(0.)
|
||||||
.max(1.) // Defined for the 0-1 range
|
.max(1.)
|
||||||
.disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active),
|
.disabled(!coherent_noise_active || !fractal_active || domain_warp_only_fractal_type_wrongly_active),
|
||||||
|
SliderRange { min: 0., max: 1., default: Some(0.) },
|
||||||
);
|
);
|
||||||
Ok(vec![fractal_weighted_strength.into()])
|
Ok(vec![fractal_weighted_strength.into()])
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -178,6 +178,24 @@ pub(crate) struct NumberOptions {
|
|||||||
pub slider: bool,
|
pub slider: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The values a range slider's two ends map to linearly and the one its double-click restores, if known.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct SliderRange {
|
||||||
|
pub min: f64,
|
||||||
|
pub max: f64,
|
||||||
|
pub default: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SliderRange {
|
||||||
|
fn position(self, value: f64) -> f64 {
|
||||||
|
((value - self.min) / (self.max - self.min)).clamp(0., 1.)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(self, position: f64) -> f64 {
|
||||||
|
(self.min + position * (self.max - self.min)).clamp(self.min, self.max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn property_from_type(
|
pub(crate) fn property_from_type(
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
index: usize,
|
index: usize,
|
||||||
@@ -991,43 +1009,37 @@ pub fn progression_widget(parameter_widgets_info: ParameterWidgetsInfo, number_p
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `parameter_widgets_info` is for the f64 parameter. `bool_input_index` is the input index of the bool parameter for the checkbox.
|
/// `parameter_widgets_info` is for the f64 parameter. `bool_input_index` is the input index of the bool parameter for the checkbox.
|
||||||
pub fn optional_f64_widget(parameter_widgets_info: ParameterWidgetsInfo, bool_input_index: usize, number_props: NumberInput) -> Vec<WidgetInstance> {
|
/// A number row gated by the bool input at `bool_input_index`, drawn as a checkbox in the assist slot after the label like the
|
||||||
let ParameterWidgetsInfo {
|
/// Opacity node's toggles, so the caller passes `blank_assist = false`. Given a `slider`, a range slider spanning it sits between them.
|
||||||
document_node,
|
pub fn optional_f64_widget(parameter_widgets_info: ParameterWidgetsInfo, bool_input_index: usize, number_props: NumberInput, slider: Option<SliderRange>) -> Vec<WidgetInstance> {
|
||||||
node_id,
|
let node_id = parameter_widgets_info.node_id;
|
||||||
index: number_input_index,
|
let enabled = parameter_widgets_info
|
||||||
..
|
.document_node
|
||||||
} = parameter_widgets_info;
|
.and_then(|document_node| document_node.inputs.get(bool_input_index))
|
||||||
|
.and_then(|input| input.as_non_exposed_value())
|
||||||
|
.and_then(|value| if let TaggedValue::Bool(enabled) = value { Some(*enabled) } else { None });
|
||||||
|
let label_count = start_widgets(¶meter_widgets_info).len();
|
||||||
|
let exposed = parameter_widgets_info.is_exposed();
|
||||||
|
|
||||||
let mut widgets = start_widgets(¶meter_widgets_info);
|
let number_props = number_props.disabled(enabled == Some(false));
|
||||||
|
let mut widgets = match slider {
|
||||||
|
Some(slider) => range_slider_widget(parameter_widgets_info, number_props, slider),
|
||||||
|
None => number_widget(parameter_widgets_info, number_props),
|
||||||
|
};
|
||||||
|
|
||||||
let Some(document_node) = document_node else { return Vec::new() };
|
if let Some(enabled) = enabled
|
||||||
let Some(number_input) = document_node.inputs.get(number_input_index) else {
|
&& !exposed
|
||||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
{
|
||||||
return vec![];
|
let checkbox = [
|
||||||
};
|
|
||||||
let Some(bool_input) = document_node.inputs.get(bool_input_index) else {
|
|
||||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
|
||||||
return vec![];
|
|
||||||
};
|
|
||||||
if let (Some(&TaggedValue::Bool(enabled)), Some(&TaggedValue::F64(number))) = (bool_input.as_non_exposed_value(), number_input.as_non_exposed_value()) {
|
|
||||||
widgets.extend_from_slice(&[
|
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
// The checkbox toggles if the value is Some or None
|
|
||||||
CheckboxInput::new(enabled)
|
CheckboxInput::new(enabled)
|
||||||
.on_update(update_value_at_index(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, bool_input_index))
|
.on_update(update_value_at_index(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, bool_input_index))
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
];
|
||||||
number_props
|
widgets.splice(label_count..label_count, checkbox);
|
||||||
.value(Some(number))
|
|
||||||
.on_update(update_value_at_index(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap_or_default()), node_id, number_input_index))
|
|
||||||
.disabled(!enabled)
|
|
||||||
.on_commit(commit_value)
|
|
||||||
.widget_instance(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets
|
widgets
|
||||||
@@ -1567,6 +1579,103 @@ pub(crate) fn hue_saturation_properties(node_id: NodeId, context: &mut NodePrope
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single-marker `SpectrumInput` over `track` driving the number at `input_index`: the marker sits at `position`, double-click
|
||||||
|
/// returns it to `default_position`, and each move sets the input to `value_at` the new position.
|
||||||
|
fn value_slider(
|
||||||
|
node_id: NodeId,
|
||||||
|
input_index: usize,
|
||||||
|
track: GradientStops<SRGBA8>,
|
||||||
|
handle_color: Color,
|
||||||
|
position: f64,
|
||||||
|
default_position: Option<f64>,
|
||||||
|
value_at: impl Fn(f64) -> TaggedValue + 'static + Send + Sync,
|
||||||
|
) -> SpectrumInput {
|
||||||
|
SpectrumInput::new(track)
|
||||||
|
.track_space(GradientSpace::RgbGamma)
|
||||||
|
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
||||||
|
.show_midpoints(false)
|
||||||
|
.allow_insert(false)
|
||||||
|
.allow_delete(false)
|
||||||
|
.allow_reorder(false)
|
||||||
|
.on_update(move |update: &SpectrumInputUpdate| {
|
||||||
|
let new_position = match update {
|
||||||
|
SpectrumInputUpdate::MoveMarker { index: 0, position } => Some(*position),
|
||||||
|
SpectrumInputUpdate::ResetMarker { index: 0 } => default_position,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(new_position) = new_position else { return Message::NoOp };
|
||||||
|
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index,
|
||||||
|
value: value_at(new_position).into(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.on_commit(commit_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A row with a range slider and a 60px number input for the number at `parameter_widgets_info`. The slider's 0..1 position maps
|
||||||
|
/// to the number through `position_of` and `value_at`, and double-click restores `default`.
|
||||||
|
fn slider_row(
|
||||||
|
parameter_widgets_info: ParameterWidgetsInfo,
|
||||||
|
number_props: NumberInput,
|
||||||
|
default: Option<f64>,
|
||||||
|
position_of: impl Fn(f64) -> f64,
|
||||||
|
value_at: impl Fn(f64) -> f64 + 'static + Send + Sync,
|
||||||
|
) -> Vec<WidgetInstance> {
|
||||||
|
let mut widgets = start_widgets(¶meter_widgets_info);
|
||||||
|
|
||||||
|
let Some(input) = parameter_widgets_info.input() else {
|
||||||
|
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
// An exposed input shows only its label and source
|
||||||
|
let (current, tagged_value): (f64, fn(f64) -> TaggedValue) = match input.as_non_exposed_value() {
|
||||||
|
Some(&TaggedValue::F64(value)) => (value, TaggedValue::F64),
|
||||||
|
Some(&TaggedValue::F32(value)) => (value as f64, |value| TaggedValue::F32(value as f32)),
|
||||||
|
_ => return widgets,
|
||||||
|
};
|
||||||
|
let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
|
||||||
|
|
||||||
|
widgets.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
value_slider(
|
||||||
|
node_id,
|
||||||
|
index,
|
||||||
|
GradientStops::default(),
|
||||||
|
Color::WHITE,
|
||||||
|
position_of(current),
|
||||||
|
default.map(position_of),
|
||||||
|
move |position| tagged_value(value_at(position)),
|
||||||
|
)
|
||||||
|
.range_slider(true)
|
||||||
|
.disabled(number_props.disabled)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
number_props
|
||||||
|
.value(Some(current))
|
||||||
|
.min_width(60)
|
||||||
|
.max_width(60)
|
||||||
|
.on_update(update_value_at_index(move |x: &NumberInput| tagged_value(x.value.unwrap_or_default()), node_id, index))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_instance(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A slider row running linearly across `slider`'s bounds.
|
||||||
|
pub(crate) fn range_slider_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: NumberInput, slider: SliderRange) -> Vec<WidgetInstance> {
|
||||||
|
slider_row(
|
||||||
|
parameter_widgets_info,
|
||||||
|
number_props,
|
||||||
|
slider.default,
|
||||||
|
move |value| slider.position(value),
|
||||||
|
move |position| slider.value(position),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a row with a single-marker `SpectrumInput` and a 60px `NumberInput`. The marker maps `value_min..value_max` to position 0..1, and double-click resets to `default_value`.
|
/// Build a row with a single-marker `SpectrumInput` and a 60px `NumberInput`. The marker maps `value_min..value_max` to position 0..1, and double-click resets to `default_value`.
|
||||||
fn spectrum_slider_row(
|
fn spectrum_slider_row(
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
@@ -1590,37 +1699,26 @@ fn spectrum_slider_row(
|
|||||||
|
|
||||||
// Only add the spectrum and number widgets when the input is not exposed
|
// Only add the spectrum and number widgets when the input is not exposed
|
||||||
if let Some(current) = current {
|
if let Some(current) = current {
|
||||||
let value_range = value_max - value_min;
|
let slider = SliderRange {
|
||||||
let position = ((current - value_min) / value_range).clamp(0., 1.);
|
min: value_min,
|
||||||
let default_position = ((default_value - value_min) / value_range).clamp(0., 1.);
|
max: value_max,
|
||||||
|
default: Some(default_value),
|
||||||
|
};
|
||||||
|
let value_at = move |position| TaggedValue::F32(slider.value(position) as f32);
|
||||||
|
|
||||||
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
|
|
||||||
let position_to_value = move |position: f64| value_min + position * value_range;
|
|
||||||
row.push(
|
row.push(
|
||||||
SpectrumInput::new(GradientStops::from(&track))
|
value_slider(
|
||||||
.track_space(GradientSpace::RgbGamma)
|
node_id,
|
||||||
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
input_index,
|
||||||
.show_midpoints(false)
|
GradientStops::from(&track),
|
||||||
.allow_insert(false)
|
handle_color,
|
||||||
.allow_delete(false)
|
slider.position(current),
|
||||||
.allow_reorder(false)
|
Some(slider.position(default_value)),
|
||||||
.narrow(true)
|
value_at,
|
||||||
.on_update(move |update: &SpectrumInputUpdate| {
|
)
|
||||||
let new_position = match update {
|
.narrow(true)
|
||||||
SpectrumInputUpdate::MoveMarker { index: 0, position } => *position,
|
.widget_instance(),
|
||||||
SpectrumInputUpdate::ResetMarker { index: 0 } => default_position,
|
|
||||||
_ => return Message::NoOp,
|
|
||||||
};
|
|
||||||
NodeGraphMessage::SetInputValue {
|
|
||||||
node_id,
|
|
||||||
input_index,
|
|
||||||
value: TaggedValue::F32(position_to_value(new_position).clamp(value_min, value_max) as f32).into(),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.on_commit(commit_value)
|
|
||||||
.widget_instance(),
|
|
||||||
);
|
);
|
||||||
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
row.push(
|
row.push(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
export let allowDelete = true;
|
export let allowDelete = true;
|
||||||
export let allowReorder = true;
|
export let allowReorder = true;
|
||||||
export let narrow = false;
|
export let narrow = false;
|
||||||
|
export let rangeSlider = false;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
// Reference to the marker track DOM element so we can convert pointer coordinates to a 0..1 position along the track.
|
// Reference to the marker track DOM element so we can convert pointer coordinates to a 0..1 position along the track.
|
||||||
@@ -39,8 +40,8 @@
|
|||||||
// Active marker selection at drag start, restored if the drag is cancelled.
|
// Active marker selection at drag start, restored if the drag is cancelled.
|
||||||
let activeMarkerIndexRestore: number | undefined = undefined;
|
let activeMarkerIndexRestore: number | undefined = undefined;
|
||||||
let activeMarkerIsMidpointRestore = false;
|
let activeMarkerIsMidpointRestore = false;
|
||||||
// Tracks whether a midpoint drag has actually moved by at least one frame, to distinguish click-to-select from drag.
|
// Whether the current or last drag moved anything, so the double-click a drag's second press can produce resets nothing.
|
||||||
let midpointDragged = false;
|
let dragMoved = false;
|
||||||
// Mirrors whether Alt is currently held during the drag (the desired state).
|
// Mirrors whether Alt is currently held during the drag (the desired state).
|
||||||
let duplicateRequested = false;
|
let duplicateRequested = false;
|
||||||
// Mirrors whether a frozen copy currently exists in the gradient (the materialized state).
|
// Mirrors whether a frozen copy currently exists in the gradient (the materialized state).
|
||||||
@@ -72,19 +73,28 @@
|
|||||||
return Math.max(lower, Math.min(upper, position));
|
return Math.max(lower, Math.min(upper, position));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The nearest of the markers drawn on the track, skipping any outside 0..1 as the template does
|
||||||
|
function nearestMarkerIndex(position: number): number | undefined {
|
||||||
|
let nearest: number | undefined = undefined;
|
||||||
|
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
markers.forEach((marker, index) => {
|
||||||
|
if (marker.position < 0 || marker.position > 1) return;
|
||||||
|
const distance = Math.abs(marker.position - position);
|
||||||
|
if (distance < nearestDistance) {
|
||||||
|
nearestDistance = distance;
|
||||||
|
nearest = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
|
||||||
function markerPointerDown(e: PointerEvent, index: number) {
|
function markerPointerDown(e: PointerEvent, index: number) {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
|
|
||||||
if (e.button === BUTTON_LEFT) {
|
if (e.button === BUTTON_LEFT) {
|
||||||
activeMarkerIndexRestore = activeMarkerIndex;
|
beginMarkerDrag(e, index);
|
||||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
|
||||||
dragRestorePosition = markers[index].position;
|
|
||||||
dragInsertedMarker = false;
|
|
||||||
// Only offer duplication where new stops are allowed. Don't materialize yet: wait for the first move so an Alt-click without a drag leaves no stray copy.
|
|
||||||
duplicateRequested = allowInsert && e.altKey;
|
|
||||||
duplicateActive = false;
|
|
||||||
setActive(index, false);
|
|
||||||
addEvents();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,11 +103,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function beginMarkerDrag(e: PointerEvent, index: number) {
|
||||||
|
activeMarkerIndexRestore = activeMarkerIndex;
|
||||||
|
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||||
|
dragRestorePosition = markers[index].position;
|
||||||
|
dragInsertedMarker = false;
|
||||||
|
dragMoved = false;
|
||||||
|
// Only offer duplication where new stops are allowed. Don't materialize yet: wait for the first move so an Alt-click without a drag leaves no stray copy.
|
||||||
|
duplicateRequested = allowInsert && e.altKey;
|
||||||
|
duplicateActive = false;
|
||||||
|
setActive(index, false);
|
||||||
|
addEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Picks up the marker at `index` and carries it to the pointer
|
||||||
|
function pickUpMarker(e: PointerEvent, index: number) {
|
||||||
|
beginMarkerDrag(e, index);
|
||||||
|
moveActiveMarker(e);
|
||||||
|
}
|
||||||
|
|
||||||
function midpointPointerDown(e: PointerEvent, index: number) {
|
function midpointPointerDown(e: PointerEvent, index: number) {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
if (e.button !== BUTTON_LEFT) return;
|
if (e.button !== BUTTON_LEFT) return;
|
||||||
|
|
||||||
midpointDragged = false;
|
dragMoved = false;
|
||||||
activeMarkerIndexRestore = activeMarkerIndex;
|
activeMarkerIndexRestore = activeMarkerIndex;
|
||||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||||
dragRestorePosition = markers[index].midpoint;
|
dragRestorePosition = markers[index].midpoint;
|
||||||
@@ -106,23 +135,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function midpointDoubleClick(index: number) {
|
function midpointDoubleClick(index: number) {
|
||||||
if (disabled || midpointDragged) return;
|
if (disabled || dragMoved) return;
|
||||||
emit({ ResetMidpoint: { index } });
|
emit({ ResetMidpoint: { index } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function markerDoubleClick(index: number) {
|
function markerDoubleClick(index: number) {
|
||||||
if (disabled) return;
|
if (disabled || dragMoved) return;
|
||||||
emit({ ResetMarker: { index } });
|
emit({ ResetMarker: { index } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackPointerDown(e: PointerEvent) {
|
function trackPointerDown(e: PointerEvent) {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
if (e.button !== BUTTON_LEFT) return;
|
if (e.button !== BUTTON_LEFT) return;
|
||||||
if (!allowInsert) return;
|
|
||||||
|
|
||||||
const position = pointerPosition(e);
|
const position = pointerPosition(e);
|
||||||
if (position === undefined) return;
|
if (position === undefined) return;
|
||||||
|
|
||||||
|
// Where nothing can be inserted, the click picks up the nearest marker instead
|
||||||
|
if (!allowInsert) {
|
||||||
|
const index = nearestMarkerIndex(position);
|
||||||
|
if (index !== undefined) pickUpMarker(e, index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Compute the index this marker will land at after Rust inserts it (matches Rust's `insert_stop` logic).
|
// Compute the index this marker will land at after Rust inserts it (matches Rust's `insert_stop` logic).
|
||||||
let insertIndex = markers.findIndex((m) => m.position > position);
|
let insertIndex = markers.findIndex((m) => m.position > position);
|
||||||
if (insertIndex === -1) insertIndex = markers.length;
|
if (insertIndex === -1) insertIndex = markers.length;
|
||||||
@@ -133,6 +168,7 @@
|
|||||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||||
dragRestorePosition = position;
|
dragRestorePosition = position;
|
||||||
dragInsertedMarker = true;
|
dragInsertedMarker = true;
|
||||||
|
dragMoved = false;
|
||||||
// A stop being created by this drag can't be duplicated; duplication is only for dragging an existing stop.
|
// A stop being created by this drag can't be duplicated; duplication is only for dragging an existing stop.
|
||||||
duplicateRequested = false;
|
duplicateRequested = false;
|
||||||
duplicateActive = false;
|
duplicateActive = false;
|
||||||
@@ -142,6 +178,12 @@
|
|||||||
addEvents();
|
addEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The lane the handles hang in picks up the nearest marker like the strip does, except where the click landed on a marker itself
|
||||||
|
function markerTrackPointerDown(e: PointerEvent) {
|
||||||
|
if (e.target !== e.currentTarget) return;
|
||||||
|
trackPointerDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
function deleteShortcut(e: KeyboardEvent) {
|
function deleteShortcut(e: KeyboardEvent) {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||||
@@ -231,6 +273,7 @@
|
|||||||
if (position === undefined) return;
|
if (position === undefined) return;
|
||||||
if (!allowReorder) position = clampToNeighbors(activeMarkerIndex, position);
|
if (!allowReorder) position = clampToNeighbors(activeMarkerIndex, position);
|
||||||
|
|
||||||
|
dragMoved = true;
|
||||||
if (!dragInsertedMarker) dispatch("dragging", true);
|
if (!dragInsertedMarker) dispatch("dragging", true);
|
||||||
emit({ MoveMarker: { index: activeMarkerIndex, position } });
|
emit({ MoveMarker: { index: activeMarkerIndex, position } });
|
||||||
}
|
}
|
||||||
@@ -265,7 +308,7 @@
|
|||||||
}
|
}
|
||||||
const local = absolute < deadZoneSplit ? absolute + 1 - left : absolute - left;
|
const local = absolute < deadZoneSplit ? absolute + 1 - left : absolute - left;
|
||||||
|
|
||||||
midpointDragged = true;
|
dragMoved = true;
|
||||||
dispatch("dragging", true);
|
dispatch("dragging", true);
|
||||||
emit({ MoveMidpoint: { index: activeMarkerIndex, position: local / range } });
|
emit({ MoveMidpoint: { index: activeMarkerIndex, position: local / range } });
|
||||||
}
|
}
|
||||||
@@ -303,7 +346,6 @@
|
|||||||
dragInsertedMarker = false;
|
dragInsertedMarker = false;
|
||||||
activeMarkerIndexRestore = undefined;
|
activeMarkerIndexRestore = undefined;
|
||||||
activeMarkerIsMidpointRestore = false;
|
activeMarkerIsMidpointRestore = false;
|
||||||
midpointDragged = false;
|
|
||||||
duplicateRequested = false;
|
duplicateRequested = false;
|
||||||
duplicateActive = false;
|
duplicateActive = false;
|
||||||
skipNextMove = false;
|
skipNextMove = false;
|
||||||
@@ -395,22 +437,24 @@
|
|||||||
|
|
||||||
<LayoutCol
|
<LayoutCol
|
||||||
class="spectrum-input"
|
class="spectrum-input"
|
||||||
classes={{ narrow, disabled }}
|
classes={{ narrow, disabled, "range-slider": rangeSlider }}
|
||||||
styles={{
|
styles={{
|
||||||
"--gradient-start": trackStartCSS,
|
"--gradient-start": trackStartCSS,
|
||||||
"--gradient-end": trackEndCSS,
|
"--gradient-end": trackEndCSS,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LayoutRow class="gradient-strip" on:pointerdown={trackPointerDown}>
|
<LayoutRow class="gradient-strip" on:pointerdown={trackPointerDown}>
|
||||||
<!-- An SVG gradient interpolates its stops with straight alpha, matching the renderers, where a CSS gradient would premultiply -->
|
{#if !rangeSlider}
|
||||||
<svg class="strip-gradient" xmlns="http://www.w3.org/2000/svg">
|
<!-- An SVG gradient interpolates its stops with straight alpha, matching the renderers, where a CSS gradient would premultiply -->
|
||||||
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0">
|
<svg class="strip-gradient" xmlns="http://www.w3.org/2000/svg">
|
||||||
{#each trackSamples as sample}
|
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0">
|
||||||
<stop offset={sample.position} stop-color={sample.color} stop-opacity={sample.alpha} />
|
{#each trackSamples as sample}
|
||||||
{/each}
|
<stop offset={sample.position} stop-color={sample.color} stop-opacity={sample.alpha} />
|
||||||
</linearGradient>
|
{/each}
|
||||||
<rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
</linearGradient>
|
||||||
</svg>
|
<rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="midpoint-track">
|
<LayoutRow class="midpoint-track">
|
||||||
{#each midpointPositions as midpoint, index}
|
{#each midpointPositions as midpoint, index}
|
||||||
@@ -430,7 +474,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="marker-track" bind:this={markerTrackElement}>
|
<LayoutRow class="marker-track" classes={{ interactive: !allowInsert }} bind:this={markerTrackElement} on:pointerdown={markerTrackPointerDown}>
|
||||||
{#each markers as marker, index}
|
{#each markers as marker, index}
|
||||||
{#if marker.position >= 0 && marker.position <= 1}
|
{#if marker.position >= 0 && marker.position <= 1}
|
||||||
<svg
|
<svg
|
||||||
@@ -496,12 +540,30 @@
|
|||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A flat 4px track at the row's center, with the handle raised 4px further into it than the gradient's
|
||||||
|
&.range-slider {
|
||||||
|
.gradient-strip {
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-5-dullgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-track {
|
||||||
|
margin-top: calc(24px - 14px - 12px - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled .gradient-strip {
|
||||||
|
background: var(--color-4-dimgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lane overlaps the strip's lower half, so the whole widget carries the hover rather than the strip alone
|
||||||
&.disabled .gradient-strip {
|
&.disabled .gradient-strip {
|
||||||
transition: opacity 0.1s;
|
transition: opacity 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&.disabled:hover .gradient-strip {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.midpoint-track {
|
.midpoint-track {
|
||||||
@@ -541,9 +603,15 @@
|
|||||||
margin-top: calc(24px - 16px - 12px);
|
margin-top: calc(24px - 16px - 12px);
|
||||||
margin-left: var(--marker-half-width);
|
margin-left: var(--marker-half-width);
|
||||||
width: calc(100% - 2 * var(--marker-half-width));
|
width: calc(100% - 2 * var(--marker-half-width));
|
||||||
|
height: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
// Only where clicks pick up the nearest marker, so insertion keeps the strip's overlapped bottom edge
|
||||||
|
&.interactive {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.marker {
|
.marker {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
|||||||
Reference in New Issue
Block a user