mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +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).
|
||||
#[serde(rename = "showMidpoints")]
|
||||
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")]
|
||||
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).
|
||||
@@ -673,6 +673,9 @@ pub struct SpectrumInput {
|
||||
pub allow_reorder: bool,
|
||||
/// Compact mode: 8px track height with 8px top padding, for use in rows alongside other widgets.
|
||||
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).
|
||||
pub disabled: bool,
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod document_node_derive;
|
||||
|
||||
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 crate::messages::layout::utility_types::widget_prelude::*;
|
||||
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),
|
||||
index - 1,
|
||||
number_input,
|
||||
None,
|
||||
))])
|
||||
}),
|
||||
);
|
||||
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.
|
||||
"optional_percentage".to_string(),
|
||||
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(
|
||||
ParameterWidgetsInfo::at_index(node_id, index, false, context),
|
||||
index - 1,
|
||||
number_input,
|
||||
Some(slider),
|
||||
))])
|
||||
}),
|
||||
);
|
||||
@@ -1241,13 +1248,13 @@ fn static_input_properties() -> InputProperties {
|
||||
"noise_properties_fractal_weighted_strength".to_string(),
|
||||
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_weighted_strength = node_properties::number_widget(
|
||||
let fractal_weighted_strength = node_properties::range_slider_widget(
|
||||
ParameterWidgetsInfo::at_index(node_id, index, true, context),
|
||||
NumberInput::default()
|
||||
.mode_range()
|
||||
.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),
|
||||
SliderRange { min: 0., max: 1., default: Some(0.) },
|
||||
);
|
||||
Ok(vec![fractal_weighted_strength.into()])
|
||||
}),
|
||||
|
||||
@@ -178,6 +178,24 @@ pub(crate) struct NumberOptions {
|
||||
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(
|
||||
node_id: NodeId,
|
||||
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.
|
||||
pub fn optional_f64_widget(parameter_widgets_info: ParameterWidgetsInfo, bool_input_index: usize, number_props: NumberInput) -> Vec<WidgetInstance> {
|
||||
let ParameterWidgetsInfo {
|
||||
document_node,
|
||||
node_id,
|
||||
index: number_input_index,
|
||||
..
|
||||
} = parameter_widgets_info;
|
||||
/// 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
|
||||
/// Opacity node's toggles, so the caller passes `blank_assist = false`. Given a `slider`, a range slider spanning it sits between them.
|
||||
pub fn optional_f64_widget(parameter_widgets_info: ParameterWidgetsInfo, bool_input_index: usize, number_props: NumberInput, slider: Option<SliderRange>) -> Vec<WidgetInstance> {
|
||||
let node_id = parameter_widgets_info.node_id;
|
||||
let enabled = parameter_widgets_info
|
||||
.document_node
|
||||
.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() };
|
||||
let Some(number_input) = document_node.inputs.get(number_input_index) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return vec![];
|
||||
};
|
||||
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(&[
|
||||
if let Some(enabled) = enabled
|
||||
&& !exposed
|
||||
{
|
||||
let checkbox = [
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
// The checkbox toggles if the value is Some or None
|
||||
CheckboxInput::new(enabled)
|
||||
.on_update(update_value_at_index(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, bool_input_index))
|
||||
.on_commit(commit_value)
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
number_props
|
||||
.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.splice(label_count..label_count, checkbox);
|
||||
}
|
||||
|
||||
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`.
|
||||
fn spectrum_slider_row(
|
||||
node_id: NodeId,
|
||||
@@ -1590,37 +1699,26 @@ fn spectrum_slider_row(
|
||||
|
||||
// Only add the spectrum and number widgets when the input is not exposed
|
||||
if let Some(current) = current {
|
||||
let value_range = value_max - value_min;
|
||||
let position = ((current - value_min) / value_range).clamp(0., 1.);
|
||||
let default_position = ((default_value - value_min) / value_range).clamp(0., 1.);
|
||||
let slider = SliderRange {
|
||||
min: value_min,
|
||||
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());
|
||||
|
||||
let position_to_value = move |position: f64| value_min + position * value_range;
|
||||
row.push(
|
||||
SpectrumInput::new(GradientStops::from(&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)
|
||||
.narrow(true)
|
||||
.on_update(move |update: &SpectrumInputUpdate| {
|
||||
let new_position = match update {
|
||||
SpectrumInputUpdate::MoveMarker { index: 0, position } => *position,
|
||||
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(),
|
||||
value_slider(
|
||||
node_id,
|
||||
input_index,
|
||||
GradientStops::from(&track),
|
||||
handle_color,
|
||||
slider.position(current),
|
||||
Some(slider.position(default_value)),
|
||||
value_at,
|
||||
)
|
||||
.narrow(true)
|
||||
.widget_instance(),
|
||||
);
|
||||
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
row.push(
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
export let allowDelete = true;
|
||||
export let allowReorder = true;
|
||||
export let narrow = false;
|
||||
export let rangeSlider = 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.
|
||||
@@ -39,8 +40,8 @@
|
||||
// Active marker selection at drag start, restored if the drag is cancelled.
|
||||
let activeMarkerIndexRestore: number | undefined = undefined;
|
||||
let activeMarkerIsMidpointRestore = false;
|
||||
// Tracks whether a midpoint drag has actually moved by at least one frame, to distinguish click-to-select from drag.
|
||||
let midpointDragged = false;
|
||||
// Whether the current or last drag moved anything, so the double-click a drag's second press can produce resets nothing.
|
||||
let dragMoved = false;
|
||||
// Mirrors whether Alt is currently held during the drag (the desired state).
|
||||
let duplicateRequested = false;
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (disabled) return;
|
||||
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
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();
|
||||
beginMarkerDrag(e, index);
|
||||
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) {
|
||||
if (disabled) return;
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
midpointDragged = false;
|
||||
dragMoved = false;
|
||||
activeMarkerIndexRestore = activeMarkerIndex;
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
dragRestorePosition = markers[index].midpoint;
|
||||
@@ -106,23 +135,29 @@
|
||||
}
|
||||
|
||||
function midpointDoubleClick(index: number) {
|
||||
if (disabled || midpointDragged) return;
|
||||
if (disabled || dragMoved) return;
|
||||
emit({ ResetMidpoint: { index } });
|
||||
}
|
||||
|
||||
function markerDoubleClick(index: number) {
|
||||
if (disabled) return;
|
||||
if (disabled || dragMoved) return;
|
||||
emit({ ResetMarker: { index } });
|
||||
}
|
||||
|
||||
function trackPointerDown(e: PointerEvent) {
|
||||
if (disabled) return;
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
if (!allowInsert) return;
|
||||
|
||||
const position = pointerPosition(e);
|
||||
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).
|
||||
let insertIndex = markers.findIndex((m) => m.position > position);
|
||||
if (insertIndex === -1) insertIndex = markers.length;
|
||||
@@ -133,6 +168,7 @@
|
||||
activeMarkerIsMidpointRestore = activeMarkerIsMidpoint;
|
||||
dragRestorePosition = position;
|
||||
dragInsertedMarker = true;
|
||||
dragMoved = false;
|
||||
// A stop being created by this drag can't be duplicated; duplication is only for dragging an existing stop.
|
||||
duplicateRequested = false;
|
||||
duplicateActive = false;
|
||||
@@ -142,6 +178,12 @@
|
||||
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) {
|
||||
if (disabled) return;
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
@@ -231,6 +273,7 @@
|
||||
if (position === undefined) return;
|
||||
if (!allowReorder) position = clampToNeighbors(activeMarkerIndex, position);
|
||||
|
||||
dragMoved = true;
|
||||
if (!dragInsertedMarker) dispatch("dragging", true);
|
||||
emit({ MoveMarker: { index: activeMarkerIndex, position } });
|
||||
}
|
||||
@@ -265,7 +308,7 @@
|
||||
}
|
||||
const local = absolute < deadZoneSplit ? absolute + 1 - left : absolute - left;
|
||||
|
||||
midpointDragged = true;
|
||||
dragMoved = true;
|
||||
dispatch("dragging", true);
|
||||
emit({ MoveMidpoint: { index: activeMarkerIndex, position: local / range } });
|
||||
}
|
||||
@@ -303,7 +346,6 @@
|
||||
dragInsertedMarker = false;
|
||||
activeMarkerIndexRestore = undefined;
|
||||
activeMarkerIsMidpointRestore = false;
|
||||
midpointDragged = false;
|
||||
duplicateRequested = false;
|
||||
duplicateActive = false;
|
||||
skipNextMove = false;
|
||||
@@ -395,22 +437,24 @@
|
||||
|
||||
<LayoutCol
|
||||
class="spectrum-input"
|
||||
classes={{ narrow, disabled }}
|
||||
classes={{ narrow, disabled, "range-slider": rangeSlider }}
|
||||
styles={{
|
||||
"--gradient-start": trackStartCSS,
|
||||
"--gradient-end": trackEndCSS,
|
||||
}}
|
||||
>
|
||||
<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 -->
|
||||
<svg class="strip-gradient" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0">
|
||||
{#each trackSamples as sample}
|
||||
<stop offset={sample.position} stop-color={sample.color} stop-opacity={sample.alpha} />
|
||||
{/each}
|
||||
</linearGradient>
|
||||
<rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||
</svg>
|
||||
{#if !rangeSlider}
|
||||
<!-- An SVG gradient interpolates its stops with straight alpha, matching the renderers, where a CSS gradient would premultiply -->
|
||||
<svg class="strip-gradient" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0">
|
||||
{#each trackSamples as sample}
|
||||
<stop offset={sample.position} stop-color={sample.color} stop-opacity={sample.alpha} />
|
||||
{/each}
|
||||
</linearGradient>
|
||||
<rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||
</svg>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<LayoutRow class="midpoint-track">
|
||||
{#each midpointPositions as midpoint, index}
|
||||
@@ -430,7 +474,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</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}
|
||||
{#if marker.position >= 0 && marker.position <= 1}
|
||||
<svg
|
||||
@@ -496,12 +540,30 @@
|
||||
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 {
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.disabled:hover .gradient-strip {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.midpoint-track {
|
||||
@@ -541,9 +603,15 @@
|
||||
margin-top: calc(24px - 16px - 12px);
|
||||
margin-left: var(--marker-half-width);
|
||||
width: calc(100% - 2 * var(--marker-half-width));
|
||||
height: 12px;
|
||||
position: relative;
|
||||
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 {
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
|
||||
Reference in New Issue
Block a user