Polish and add aborting to several input widgets: no Esc closing parent menus; color picker axis align; repeat on arrow buttons (#2276)
* Remove color input outline; reduce antialiasing compositing artifacts in color widgets * Rename ColorButton to ColorInput * Add features and aborting to several other widgets - Prevent Esc from closing parent floating menus when aborting - Fix missing icon regression - Gutter resizing abort - Color picker aborts, Shift axis alignment, improve click/drag behavior for gradient spectrum - Scrollbar abort, repeat when held, fix directional arrows when viewport is zoomed - Number input abort, repeat when held * Move ColorInput into the inputs folder * Fix tiny logo
This commit is contained in:
parent
0037f5158c
commit
ec8c8d6485
|
|
@ -80,7 +80,7 @@ impl LayoutMessageHandler {
|
|||
};
|
||||
responses.add(callback_message);
|
||||
}
|
||||
Widget::ColorButton(color_button) => {
|
||||
Widget::ColorInput(color_button) => {
|
||||
let callback_message = match action {
|
||||
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
||||
WidgetValueAction::Update => {
|
||||
|
|
@ -99,7 +99,7 @@ impl LayoutMessageHandler {
|
|||
};
|
||||
|
||||
(|| {
|
||||
let update_value = value.as_object().expect("ColorButton update was not of type: object");
|
||||
let update_value = value.as_object().expect("ColorInput update was not of type: object");
|
||||
|
||||
// None
|
||||
let is_none = update_value.get("none").and_then(|x| x.as_bool());
|
||||
|
|
@ -139,7 +139,7 @@ impl LayoutMessageHandler {
|
|||
return (color_button.on_update.callback)(color_button);
|
||||
}
|
||||
|
||||
panic!("ColorButton update was not able to be parsed with color data: {color_button:?}");
|
||||
panic!("ColorInput update was not able to be parsed with color data: {color_button:?}");
|
||||
})()
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ impl LayoutGroup {
|
|||
for widget in &mut widgets {
|
||||
let val = match &mut widget.widget {
|
||||
Widget::CheckboxInput(x) => &mut x.tooltip,
|
||||
Widget::ColorButton(x) => &mut x.tooltip,
|
||||
Widget::ColorInput(x) => &mut x.tooltip,
|
||||
Widget::CurveInput(x) => &mut x.tooltip,
|
||||
Widget::DropdownInput(x) => &mut x.tooltip,
|
||||
Widget::FontInput(x) => &mut x.tooltip,
|
||||
|
|
@ -498,7 +498,7 @@ impl<T> Default for WidgetCallback<T> {
|
|||
pub enum Widget {
|
||||
BreadcrumbTrailButtons(BreadcrumbTrailButtons),
|
||||
CheckboxInput(CheckboxInput),
|
||||
ColorButton(ColorButton),
|
||||
ColorInput(ColorInput),
|
||||
CurveInput(CurveInput),
|
||||
DropdownInput(DropdownInput),
|
||||
FontInput(FontInput),
|
||||
|
|
@ -571,7 +571,7 @@ impl DiffUpdate {
|
|||
let mut tooltip_shortcut = match &mut widget_holder.widget {
|
||||
Widget::BreadcrumbTrailButtons(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::ColorButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ pub struct ImageButton {
|
|||
|
||||
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct ColorButton {
|
||||
pub struct ColorInput {
|
||||
#[widget_builder(constructor)]
|
||||
pub value: FillChoice,
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ pub struct ColorButton {
|
|||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<ColorButton>,
|
||||
pub on_update: WidgetCallback<ColorInput>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ pub enum NavigationMessage {
|
|||
BeginCanvasTilt { was_dispatched_from_menu: bool },
|
||||
BeginCanvasZoom,
|
||||
CanvasPan { delta: DVec2 },
|
||||
CanvasPanAbortPrepare { x_not_y_axis: bool },
|
||||
CanvasPanAbort { x_not_y_axis: bool },
|
||||
CanvasPanByViewportFraction { delta: DVec2 },
|
||||
CanvasPanMouseWheel { use_y_as_x: bool },
|
||||
CanvasTiltResetAndZoomTo100Percent,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ pub struct NavigationMessageHandler {
|
|||
navigation_operation: NavigationOperation,
|
||||
mouse_position: ViewportPosition,
|
||||
finish_operation_with_click: bool,
|
||||
abortable_pan_start: Option<f64>,
|
||||
}
|
||||
|
||||
impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for NavigationMessageHandler {
|
||||
|
|
@ -141,6 +142,28 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
|||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
}
|
||||
NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis } => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
log::error!("Could not get PTZ in CanvasPanAbortPrepare");
|
||||
return;
|
||||
};
|
||||
self.abortable_pan_start = Some(if x_not_y_axis { ptz.pan.x } else { ptz.pan.y });
|
||||
}
|
||||
NavigationMessage::CanvasPanAbort { x_not_y_axis } => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
log::error!("Could not get PTZ in CanvasPanAbort");
|
||||
return;
|
||||
};
|
||||
if let Some(abortable_pan_start) = self.abortable_pan_start {
|
||||
if x_not_y_axis {
|
||||
ptz.pan.x = abortable_pan_start;
|
||||
} else {
|
||||
ptz.pan.y = abortable_pan_start;
|
||||
}
|
||||
}
|
||||
self.abortable_pan_start = None;
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
}
|
||||
NavigationMessage::CanvasPanByViewportFraction { delta } => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
log::error!("Could not get node graph PTZ in CanvasPanByViewportFraction");
|
||||
|
|
|
|||
|
|
@ -3303,7 +3303,7 @@ fn static_input_properties() -> InputProperties {
|
|||
"assign_colors_gradient".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?;
|
||||
let gradient_row = node_properties::color_widget(document_node, node_id, index, input_name, ColorButton::default().allow_none(false), true);
|
||||
let gradient_row = node_properties::color_widget(document_node, node_id, index, input_name, ColorInput::default().allow_none(false), true);
|
||||
Ok(vec![gradient_row])
|
||||
}),
|
||||
);
|
||||
|
|
@ -3329,7 +3329,7 @@ fn static_input_properties() -> InputProperties {
|
|||
"mask_stencil".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
let (document_node, input_name) = node_properties::query_node_and_input_name(node_id, index, context)?;
|
||||
let mask = node_properties::color_widget(document_node, node_id, index, input_name, ColorButton::default(), true);
|
||||
let mask = node_properties::color_widget(document_node, node_id, index, input_name, ColorInput::default(), true);
|
||||
Ok(vec![mask])
|
||||
}),
|
||||
);
|
||||
|
|
@ -3403,7 +3403,7 @@ fn static_input_properties() -> InputProperties {
|
|||
node_id,
|
||||
index,
|
||||
input_name,
|
||||
ColorButton::default().allow_none(false),
|
||||
ColorInput::default().allow_none(false),
|
||||
true,
|
||||
)])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1822,7 +1822,7 @@ impl NodeGraphMessageHandler {
|
|||
// If only one node is selected then show the preview or stop previewing button
|
||||
if let Some(node_id) = previewing {
|
||||
let button = TextButton::new("End Preview")
|
||||
.icon(Some("Rescale".to_string()))
|
||||
.icon(Some("FrameAll".to_string()))
|
||||
.tooltip("Restore preview to the graph output")
|
||||
.on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into())
|
||||
.widget_holder();
|
||||
|
|
@ -1834,7 +1834,7 @@ impl NodeGraphMessageHandler {
|
|||
.any(|export| matches!(export, NodeInput::Node { node_id: export_node_id, .. } if *export_node_id == node_id));
|
||||
if selection_is_not_already_the_output && no_other_selections {
|
||||
let button = TextButton::new("Preview")
|
||||
.icon(Some("Rescale".to_string()))
|
||||
.icon(Some("FrameAll".to_string()))
|
||||
.tooltip("Preview selected node/layer (Shortcut: Alt-click node/layer)")
|
||||
.on_update(move |_| NodeGraphMessage::TogglePreview { node_id }.into())
|
||||
.widget_holder();
|
||||
|
|
|
|||
|
|
@ -140,8 +140,8 @@ pub(crate) fn property_from_type(node_id: NodeId, index: usize, ty: &Type, numbe
|
|||
Some(x) if x == TypeId::of::<u32>() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX))), true).into(),
|
||||
Some(x) if x == TypeId::of::<u64>() => number_widget(document_node, node_id, index, name, number_input.int().min(min(0.)), true).into(),
|
||||
Some(x) if x == TypeId::of::<String>() => text_widget(document_node, node_id, index, name, true).into(),
|
||||
Some(x) if x == TypeId::of::<Color>() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true),
|
||||
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(true), true),
|
||||
Some(x) if x == TypeId::of::<Color>() => color_widget(document_node, node_id, index, name, ColorInput::default().allow_none(false), true),
|
||||
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(document_node, node_id, index, name, ColorInput::default().allow_none(true), true),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist),
|
||||
Some(x) if x == TypeId::of::<UVec2>() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", Some(0.), add_blank_assist),
|
||||
Some(x) if x == TypeId::of::<IVec2>() => vec2_widget(document_node, node_id, index, name, "X", "Y", "", None, add_blank_assist),
|
||||
|
|
@ -152,7 +152,7 @@ pub(crate) fn property_from_type(node_id: NodeId, index: usize, ty: &Type, numbe
|
|||
font_widgets.into_iter().chain(style_widgets.unwrap_or_default()).collect::<Vec<_>>().into()
|
||||
}
|
||||
Some(x) if x == TypeId::of::<Curve>() => curves_widget(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<GradientStops>() => color_widget(document_node, node_id, index, name, ColorButton::default().allow_none(false), true),
|
||||
Some(x) if x == TypeId::of::<GradientStops>() => color_widget(document_node, node_id, index, name, ColorInput::default().allow_none(false), true),
|
||||
Some(x) if x == TypeId::of::<VectorDataTable>() => vector_widget(document_node, node_id, index, name, true).into(),
|
||||
Some(x) if x == TypeId::of::<RasterFrame>() || x == TypeId::of::<ImageFrameTable<Color>>() || x == TypeId::of::<TextureFrame>() => {
|
||||
raster_widget(document_node, node_id, index, name, true).into()
|
||||
|
|
@ -1067,7 +1067,7 @@ pub fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: us
|
|||
LayoutGroup::Row { widgets }
|
||||
}
|
||||
|
||||
pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorButton, blank_assist: bool) -> LayoutGroup {
|
||||
pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, color_button: ColorInput, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
|
||||
// Return early with just the label if the input is exposed to the graph, meaning we don't want to show the color picker widget in the Properties panel
|
||||
|
|
@ -1080,7 +1080,7 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||
TaggedValue::Color(color) => widgets.push(
|
||||
color_button
|
||||
.value(FillChoice::Solid(*color))
|
||||
.on_update(update_value(|x: &ColorButton| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
|
||||
.on_update(update_value(|x: &ColorInput| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
),
|
||||
|
|
@ -1090,7 +1090,7 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||
Some(color) => FillChoice::Solid(*color),
|
||||
None => FillChoice::None,
|
||||
})
|
||||
.on_update(update_value(|x: &ColorButton| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index))
|
||||
.on_update(update_value(|x: &ColorInput| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
),
|
||||
|
|
@ -1098,7 +1098,7 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||
color_button
|
||||
.value(FillChoice::Gradient(x.clone()))
|
||||
.on_update(update_value(
|
||||
|x: &ColorButton| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
|
||||
|x: &ColorInput| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
|
||||
node_id,
|
||||
index,
|
||||
))
|
||||
|
|
@ -1776,7 +1776,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||
// if let Some(&TaggedValue::F64(seed)) = &input.as_non_exposed_value() {
|
||||
// widgets.extend_from_slice(&[
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// IconButton::new("Regenerate", 24)
|
||||
// IconButton::new("Resync", 24)
|
||||
// .tooltip("Set a new random seed")
|
||||
// .on_update({
|
||||
// let imaginate_node = imaginate_node.clone();
|
||||
|
|
@ -1856,7 +1856,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
|||
|
||||
// widgets.extend_from_slice(&[
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// IconButton::new("Rescale", 24)
|
||||
// IconButton::new("FrameAll", 24)
|
||||
// .tooltip("Set the layer dimensions to this resolution")
|
||||
// .on_update(move |_| DialogMessage::RequestComingSoonDialog { issue: None }.into())
|
||||
// .widget_holder(),
|
||||
|
|
@ -2211,9 +2211,9 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||
|
||||
widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets_first_row.push(
|
||||
ColorButton::default()
|
||||
ColorInput::default()
|
||||
.value(fill.clone().into())
|
||||
.on_update(move |x: &ColorButton| {
|
||||
.on_update(move |x: &ColorInput| {
|
||||
Message::Batched(Box::new([
|
||||
match &fill2 {
|
||||
Fill::None => NodeGraphMessage::SetInputValue {
|
||||
|
|
@ -2380,7 +2380,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||
let line_join_index = 6;
|
||||
let miter_limit_index = 7;
|
||||
|
||||
let color = color_widget(document_node, node_id, color_index, "Color", ColorButton::default(), true);
|
||||
let color = color_widget(document_node, node_id, color_index, "Color", ColorInput::default(), true);
|
||||
let weight = number_widget(document_node, node_id, weight_index, "Weight", NumberInput::default().unit(" px").min(0.), true);
|
||||
|
||||
let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() {
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
})
|
||||
};
|
||||
let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| {
|
||||
update_val::<ColorButton>(grid, move |grid, color| {
|
||||
update_val::<ColorInput>(grid, move |grid, color| {
|
||||
if let FillChoice::Solid(color) = color.value {
|
||||
if let Some(update_color) = update(grid) {
|
||||
*update_color = color;
|
||||
|
|
@ -280,7 +280,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
]);
|
||||
color_widgets.push(
|
||||
ColorButton::new(FillChoice::Solid(grid.grid_color))
|
||||
ColorInput::new(FillChoice::Solid(grid.grid_color))
|
||||
.tooltip("Grid display color")
|
||||
.allow_none(false)
|
||||
.on_update(update_color(grid, |grid| Some(&mut grid.grid_color)))
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ impl ToolColorOptions {
|
|||
color_allow_none: bool,
|
||||
reset_callback: impl Fn(&IconButton) -> Message + 'static + Send + Sync,
|
||||
radio_callback: fn(ToolColorType) -> WidgetCallback<()>,
|
||||
color_callback: impl Fn(&ColorButton) -> Message + 'static + Send + Sync,
|
||||
color_callback: impl Fn(&ColorInput) -> Message + 'static + Send + Sync,
|
||||
) -> Vec<WidgetHolder> {
|
||||
let mut widgets = vec![TextLabel::new(label_text).widget_holder()];
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ impl ToolColorOptions {
|
|||
widgets.push(radio);
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
|
||||
let color_button = ColorButton::new(FillChoice::from_optional_color(self.active_color()))
|
||||
let color_button = ColorInput::new(FillChoice::from_optional_color(self.active_color()))
|
||||
.allow_none(color_allow_none)
|
||||
.on_update(color_callback);
|
||||
widgets.push(color_button.widget_holder());
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ impl LayoutHolder for BrushTool {
|
|||
false,
|
||||
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(),
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ impl LayoutHolder for EllipseTool {
|
|||
true,
|
||||
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
|
@ -101,7 +101,7 @@ impl LayoutHolder for EllipseTool {
|
|||
true,
|
||||
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ impl LayoutHolder for FreehandTool {
|
|||
true,
|
||||
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
|
@ -109,7 +109,7 @@ impl LayoutHolder for FreehandTool {
|
|||
true,
|
||||
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ impl LayoutHolder for LineTool {
|
|||
true,
|
||||
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
);
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ impl LayoutHolder for PenTool {
|
|||
true,
|
||||
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
|
@ -124,7 +124,7 @@ impl LayoutHolder for PenTool {
|
|||
true,
|
||||
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ impl LayoutHolder for PolygonTool {
|
|||
true,
|
||||
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
|
@ -145,7 +145,7 @@ impl LayoutHolder for PolygonTool {
|
|||
true,
|
||||
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ impl LayoutHolder for RectangleTool {
|
|||
true,
|
||||
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
|
@ -87,7 +87,7 @@ impl LayoutHolder for RectangleTool {
|
|||
true,
|
||||
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ impl LayoutHolder for SplineTool {
|
|||
true,
|
||||
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
|
@ -112,7 +112,7 @@ impl LayoutHolder for SplineTool {
|
|||
true,
|
||||
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ impl LayoutHolder for TextTool {
|
|||
true,
|
||||
|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorButton| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
|color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(),
|
||||
));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@
|
|||
linear-gradient(45deg, #cccccc 25%, transparent 25%, transparent 75%, #cccccc 75%), linear-gradient(#ffffff, #ffffff);
|
||||
--color-transparent-checkered-background-size: 16px 16px, 16px 16px, 16px 16px;
|
||||
--color-transparent-checkered-background-position: 0 0, 8px 8px, 8px 8px;
|
||||
--color-transparent-checkered-background-position-plus-one: 1px 1px, 9px 9px, 9px 9px;
|
||||
--color-transparent-checkered-background-size-mini: 8px 8px, 8px 8px, 8px 8px;
|
||||
--color-transparent-checkered-background-position-mini: 0 0, 4px 4px, 4px 4px;
|
||||
--color-transparent-checkered-background-repeat: repeat, repeat, repeat;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { HSV, RGB, FillChoice } from "@graphite/messages";
|
||||
import { Color, Gradient } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
|
|
@ -65,7 +66,19 @@
|
|||
// Transient state
|
||||
let draggingPickerTrack: HTMLDivElement | undefined = undefined;
|
||||
let strayCloses = true;
|
||||
let gradientSpectrumDragging = false;
|
||||
let shiftPressed = false;
|
||||
let alignedAxis: "saturation" | "value" | undefined = undefined;
|
||||
let hueBeforeDrag = 0;
|
||||
let saturationBeforeDrag = 0;
|
||||
let valueBeforeDrag = 0;
|
||||
let alphaBeforeDrag = 0;
|
||||
let saturationStartOfAxisAlign: number | undefined = undefined;
|
||||
let valueStartOfAxisAlign: number | undefined = undefined;
|
||||
let saturationRestoreWhenShiftReleased: number | undefined = undefined;
|
||||
let valueRestoreWhenShiftReleased: number | undefined = undefined;
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
let hexCodeInputWidget: TextInput | undefined;
|
||||
let gradientSpectrumInputWidget: SpectrumInput | undefined;
|
||||
|
||||
|
|
@ -77,6 +90,9 @@
|
|||
$: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
||||
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
|
||||
$: opaqueHueColor = new Color({ h: hue, s: 1, v: 1, a: 1 });
|
||||
$: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01));
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1;
|
||||
|
||||
function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
|
||||
if (none) return new Color("none");
|
||||
|
|
@ -119,6 +135,14 @@
|
|||
const target = (e.target || undefined) as HTMLElement | undefined;
|
||||
draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
|
||||
|
||||
hueBeforeDrag = hue;
|
||||
saturationBeforeDrag = saturation;
|
||||
valueBeforeDrag = value;
|
||||
alphaBeforeDrag = alpha;
|
||||
|
||||
saturationStartOfAxisAlign = undefined;
|
||||
valueStartOfAxisAlign = undefined;
|
||||
|
||||
addEvents();
|
||||
|
||||
onPointerMove(e);
|
||||
|
|
@ -134,6 +158,8 @@
|
|||
saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
|
||||
value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
||||
strayCloses = false;
|
||||
|
||||
if (shiftPressed) updateAxisLock();
|
||||
} else if (draggingPickerTrack?.hasAttribute("data-hue-picker")) {
|
||||
const rectangle = draggingPickerTrack.getBoundingClientRect();
|
||||
|
||||
|
|
@ -148,25 +174,102 @@
|
|||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
setColor(color);
|
||||
|
||||
if (!e.shiftKey) {
|
||||
shiftPressed = false;
|
||||
alignedAxis = undefined;
|
||||
} else if (!shiftPressed && draggingPickerTrack) {
|
||||
shiftPressed = true;
|
||||
saturationStartOfAxisAlign = saturation;
|
||||
valueStartOfAxisAlign = value;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortDrag();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
const element = self?.div();
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
abortDrag();
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e: KeyboardEvent) {
|
||||
if (e.key === "Shift") {
|
||||
shiftPressed = false;
|
||||
alignedAxis = undefined;
|
||||
|
||||
if (saturationRestoreWhenShiftReleased !== undefined && valueRestoreWhenShiftReleased !== undefined) {
|
||||
saturation = saturationRestoreWhenShiftReleased;
|
||||
value = valueRestoreWhenShiftReleased;
|
||||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addEvents() {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
|
||||
dispatch("startHistoryTransaction");
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
draggingPickerTrack = undefined;
|
||||
strayCloses = true;
|
||||
// The setTimeout is necessary to prevent the FloatingMenu's `escapeCloses` from becoming true immediately upon pressing the Escape key, and thus closing
|
||||
setTimeout(() => (strayCloses = true), 0);
|
||||
shiftPressed = false;
|
||||
alignedAxis = undefined;
|
||||
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("keyup", onKeyUp);
|
||||
}
|
||||
|
||||
function updateAxisLock() {
|
||||
if (!saturationStartOfAxisAlign || !valueStartOfAxisAlign) return;
|
||||
|
||||
const deltaSaturation = saturation - saturationStartOfAxisAlign;
|
||||
const deltaValue = value - valueStartOfAxisAlign;
|
||||
|
||||
saturationRestoreWhenShiftReleased = saturation;
|
||||
valueRestoreWhenShiftReleased = value;
|
||||
|
||||
if (Math.abs(deltaSaturation) < Math.abs(deltaValue)) {
|
||||
alignedAxis = "saturation";
|
||||
saturation = saturationStartOfAxisAlign;
|
||||
} else {
|
||||
alignedAxis = "value";
|
||||
value = valueStartOfAxisAlign;
|
||||
}
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
removeEvents();
|
||||
|
||||
hue = hueBeforeDrag;
|
||||
saturation = saturationBeforeDrag;
|
||||
value = valueBeforeDrag;
|
||||
alpha = alphaBeforeDrag;
|
||||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
setColor(color);
|
||||
}
|
||||
|
||||
function setColor(color?: Color) {
|
||||
|
|
@ -299,7 +402,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<FloatingMenu class="color-picker" {open} on:open {strayCloses} {direction} type="Popover">
|
||||
<FloatingMenu class="color-picker" {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||
<LayoutRow
|
||||
styles={{
|
||||
"--new-color": newColor.toHexOptionalAlpha(),
|
||||
|
|
@ -318,6 +421,15 @@
|
|||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||
{/if}
|
||||
{#if alignedAxis}
|
||||
<div
|
||||
class="selection-circle-alignment"
|
||||
class:saturation={alignedAxis === "saturation"}
|
||||
class:value={alignedAxis === "value"}
|
||||
style:top={`${(1 - value) * 100}%`}
|
||||
style:left={`${saturation * 100}%`}
|
||||
/>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
|
||||
{#if !isNone}
|
||||
|
|
@ -340,6 +452,7 @@
|
|||
}}
|
||||
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||
activeMarkerIndex={activeIndex}
|
||||
on:dragging={({ detail }) => (gradientSpectrumDragging = detail)}
|
||||
bind:this={gradientSpectrumInputWidget}
|
||||
/>
|
||||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
|
|
@ -360,6 +473,8 @@
|
|||
<LayoutCol class="details">
|
||||
<LayoutRow
|
||||
class="choice-preview"
|
||||
classes={{ outlined, transparency }}
|
||||
styles={{ "--outline-amount": outlineFactor }}
|
||||
tooltip={!newColor.equals(oldColor) ? "Comparison between the present color choice (left) and the color before any change was made (right)" : "The present color choice"}
|
||||
>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
|
|
@ -453,7 +568,7 @@
|
|||
</LayoutRow>
|
||||
</LayoutRow>
|
||||
<LayoutRow>
|
||||
<TextLabel tooltip="Scale from transparent (0%) to opaque (100%) for the color's alpha channel">Alpha</TextLabel>
|
||||
<TextLabel tooltip="Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel">Alpha</TextLabel>
|
||||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
|
|
@ -471,18 +586,18 @@
|
|||
unit="%"
|
||||
mode="Range"
|
||||
displayDecimalPlaces={1}
|
||||
tooltip={`Scale from transparent (0%) to opaque (100%) for the color's alpha channel`}
|
||||
tooltip={`Scale of translucency, from transparent (0%) to opaque (100%), for the color's alpha channel`}
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="leftover-space" />
|
||||
<LayoutRow>
|
||||
{#if allowNone && !gradient}
|
||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0" />
|
||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
{/if}
|
||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0" />
|
||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0" />
|
||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
|
||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
|
||||
|
|
@ -501,10 +616,13 @@
|
|||
|
||||
<style lang="scss" global>
|
||||
.color-picker {
|
||||
--picker-size: 256px;
|
||||
--picker-circle-radius: 6px;
|
||||
|
||||
.pickers-and-gradient {
|
||||
.pickers {
|
||||
.saturation-value-picker {
|
||||
width: 256px;
|
||||
width: var(--picker-size);
|
||||
background-blend-mode: multiply;
|
||||
background: linear-gradient(to bottom, #ffffff, #000000), linear-gradient(to right, #ffffff, var(--hue-color));
|
||||
position: relative;
|
||||
|
|
@ -513,7 +631,7 @@
|
|||
.saturation-value-picker,
|
||||
.hue-picker,
|
||||
.alpha-picker {
|
||||
height: 256px;
|
||||
height: var(--picker-size);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
@ -552,8 +670,8 @@
|
|||
|
||||
.selection-circle {
|
||||
position: absolute;
|
||||
left: 0%;
|
||||
top: 0%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
|
|
@ -562,19 +680,59 @@
|
|||
content: "";
|
||||
display: block;
|
||||
position: relative;
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
left: calc(-1 * var(--picker-circle-radius));
|
||||
top: calc(-1 * var(--picker-circle-radius));
|
||||
width: calc(var(--picker-circle-radius) * 2 + 1px);
|
||||
height: calc(var(--picker-circle-radius) * 2 + 1px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--opaque-color-contrasting);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-circle-alignment {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
|
||||
&.saturation::before,
|
||||
&.saturation::after,
|
||||
&.value::before,
|
||||
&.value::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: var(--opaque-color-contrasting);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&.saturation {
|
||||
&::before {
|
||||
height: var(--picker-size);
|
||||
margin-top: calc(-1 * var(--picker-size) - var(--picker-circle-radius));
|
||||
}
|
||||
|
||||
&::after {
|
||||
height: var(--picker-size);
|
||||
margin-top: var(--picker-circle-radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.value {
|
||||
&::before {
|
||||
width: var(--picker-size);
|
||||
margin-left: var(--picker-circle-radius);
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: var(--picker-size);
|
||||
margin-left: calc(-1 * var(--picker-size) - var(--picker-circle-radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selection-needle {
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
|
|
@ -642,14 +800,27 @@
|
|||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--color-1-nearblack);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
|
||||
&.outlined::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--color-0-black-rgb), var(--outline-amount));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.transparency {
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
.swap-button-background {
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
<script lang="ts" context="module">
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
|
||||
|
||||
/// Prevents the escape key from closing the parent floating menu of the given element.
|
||||
/// This works by momentarily setting the `data-escape-does-not-close` attribute on the parent floating menu element.
|
||||
/// After checking for the Escape key, it checks (in one `setTimeout`) for the attribute and ignores the key if it's present.
|
||||
/// Then after two calls of `setTimeout`, we can safely remove the attribute here.
|
||||
export function preventEscapeClosingParentFloatingMenu(element: HTMLElement) {
|
||||
const floatingMenuParent = element.closest("[data-floating-menu-content]") || undefined;
|
||||
if (floatingMenuParent instanceof HTMLElement) {
|
||||
floatingMenuParent.setAttribute("data-escape-does-not-close", "");
|
||||
setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
floatingMenuParent.removeAttribute("data-escape-does-not-close");
|
||||
}, 0);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
@ -10,6 +26,7 @@
|
|||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
||||
const BUTTON_LEFT = 0;
|
||||
const POINTER_STRAY_DISTANCE = 100;
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: boolean; naturalWidth: number }>();
|
||||
|
|
@ -302,7 +319,8 @@
|
|||
}
|
||||
|
||||
// Clean up any messes from lost pointerup events
|
||||
const eventIncludesLmb = Boolean(e.buttons & 1);
|
||||
const BUTTONS_LEFT = 0b0000_0001;
|
||||
const eventIncludesLmb = Boolean(e.buttons & BUTTONS_LEFT);
|
||||
if (!open && !eventIncludesLmb) {
|
||||
pointerStillDown = false;
|
||||
window.removeEventListener("pointerup", pointerUpHandler);
|
||||
|
|
@ -377,8 +395,15 @@
|
|||
}
|
||||
|
||||
function keyDownHandler(e: KeyboardEvent) {
|
||||
if (escapeCloses && e.key.toLowerCase() === "escape") {
|
||||
dispatch("open", false);
|
||||
if (escapeCloses && e.key === "Escape") {
|
||||
setTimeout(() => {
|
||||
if (!floatingMenuContainer?.querySelector("[data-floating-menu-content][data-escape-does-not-close]")) {
|
||||
dispatch("open", false);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Find the parent floating menu and prevent it from also closing with the escape key when this floating menu does
|
||||
if (self) preventEscapeClosingParentFloatingMenu(self);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -388,13 +413,13 @@
|
|||
dispatch("open", false);
|
||||
|
||||
// Track if the left pointer button is now down so its later click event can be canceled
|
||||
const eventIsForLmb = e.button === 0;
|
||||
const eventIsForLmb = e.button === BUTTON_LEFT;
|
||||
if (eventIsForLmb) pointerStillDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
function pointerUpHandler(e: PointerEvent) {
|
||||
const eventIsForLmb = e.button === 0;
|
||||
const eventIsForLmb = e.button === BUTTON_LEFT;
|
||||
if (pointerStillDown && eventIsForLmb) {
|
||||
// Clean up self
|
||||
pointerStillDown = false;
|
||||
|
|
|
|||
|
|
@ -169,16 +169,6 @@
|
|||
editor.handle.panCanvas(0, -delta * scrollbarMultiplier.y);
|
||||
}
|
||||
|
||||
function pageX(delta: number) {
|
||||
const move = delta < 0 ? 1 : -1;
|
||||
editor.handle.panCanvasByFraction(move, 0);
|
||||
}
|
||||
|
||||
function pageY(delta: number) {
|
||||
const move = delta < 0 ? 1 : -1;
|
||||
editor.handle.panCanvasByFraction(0, move);
|
||||
}
|
||||
|
||||
function canvasPointerDown(e: PointerEvent) {
|
||||
const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable;
|
||||
|
||||
|
|
@ -549,21 +539,25 @@
|
|||
<LayoutCol class="ruler-or-scrollbar right-scrollbar">
|
||||
<ScrollbarInput
|
||||
direction="Vertical"
|
||||
handleLength={scrollbarSize.y}
|
||||
handlePosition={scrollbarPos.y}
|
||||
on:handlePosition={({ detail }) => panCanvasY(detail)}
|
||||
on:pressTrack={({ detail }) => pageY(detail)}
|
||||
thumbLength={scrollbarSize.y}
|
||||
thumbPosition={scrollbarPos.y}
|
||||
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(0, detail)}
|
||||
on:thumbPosition={({ detail }) => panCanvasY(detail)}
|
||||
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)}
|
||||
on:thumbDragAbort={() => editor.handle.panCanvasAbort(false)}
|
||||
/>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="ruler-or-scrollbar bottom-scrollbar">
|
||||
<ScrollbarInput
|
||||
direction="Horizontal"
|
||||
handleLength={scrollbarSize.x}
|
||||
handlePosition={scrollbarPos.x}
|
||||
on:handlePosition={({ detail }) => panCanvasX(detail)}
|
||||
on:pressTrack={({ detail }) => pageX(detail)}
|
||||
on:pointerup={() => editor.handle.setGridAlignedEdges()}
|
||||
thumbLength={scrollbarSize.x}
|
||||
thumbPosition={scrollbarPos.x}
|
||||
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(detail, 0)}
|
||||
on:thumbPosition={({ detail }) => panCanvasX(detail)}
|
||||
on:thumbDragEnd={() => editor.handle.setGridAlignedEdges()}
|
||||
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)}
|
||||
on:thumbDragAbort={() => editor.handle.panCanvasAbort(true)}
|
||||
/>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@
|
|||
title={listing.entry.expanded ? "Collapse" : `Expand${listing.entry.ancestorOfSelected ? "\n(A selected layer is contained within)" : ""}`}
|
||||
on:click|stopPropagation={() => handleExpandArrowClick(listing.entry.id)}
|
||||
tabindex="0"
|
||||
/>
|
||||
></button>
|
||||
{/if}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
|
||||
import ColorButton from "@graphite/components/widgets/buttons/ColorButton.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import ImageButton from "@graphite/components/widgets/buttons/ImageButton.svelte";
|
||||
import ParameterExposeButton from "@graphite/components/widgets/buttons/ParameterExposeButton.svelte";
|
||||
import PopoverButton from "@graphite/components/widgets/buttons/PopoverButton.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import CheckboxInput from "@graphite/components/widgets/inputs/CheckboxInput.svelte";
|
||||
import ColorInput from "@graphite/components/widgets/inputs/ColorInput.svelte";
|
||||
import CurveInput from "@graphite/components/widgets/inputs/CurveInput.svelte";
|
||||
import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte";
|
||||
import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte";
|
||||
|
|
@ -87,9 +87,9 @@
|
|||
{#if checkboxInput}
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorButton")}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
|
||||
{#if colorInput}
|
||||
<ColorButton {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
{/if}
|
||||
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||
{#if curvesInput}
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice } from "@graphite/messages";
|
||||
import { Color, Gradient } from "@graphite/messages";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||
|
||||
let open = false;
|
||||
|
||||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ disabled, none: value instanceof Color ? value.none : false, open }} {tooltip}>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
||||
{#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
colorOrGradient={value}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{allowNone}
|
||||
/>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.color-button {
|
||||
position: relative;
|
||||
min-width: 80px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-5-dullgray);
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
&,
|
||||
> .text-label {
|
||||
background: rgba(var(--color-6-lowergray-rgb), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
&,
|
||||
> .text-label {
|
||||
background: var(--color-4-dimgray);
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
background-image: var(--chosen-gradient), var(--color-transparent-checkered-background);
|
||||
background-size:
|
||||
100% 100%,
|
||||
var(--color-transparent-checkered-background-size);
|
||||
background-position:
|
||||
0 0,
|
||||
var(--color-transparent-checkered-background-position);
|
||||
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
&.none {
|
||||
> button {
|
||||
background: var(--color-none);
|
||||
background-repeat: var(--color-none-repeat);
|
||||
background-position: var(--color-none-position);
|
||||
background-size: var(--color-none-size-24px);
|
||||
background-image: var(--color-none-image-24px);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
> button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(var(--color-4-dimgray-rgb), 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .text-label {
|
||||
background: rgba(var(--color-5-dullgray-rgb), 0.5);
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px 0 0 6px;
|
||||
padding-right: 2px;
|
||||
padding-left: 4px;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
> .floating-menu {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||
|
||||
let open = false;
|
||||
|
||||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||
$: none = value instanceof Color ? value.none : false;
|
||||
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
{#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
</button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
colorOrGradient={value}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{allowNone}
|
||||
/>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.color-button {
|
||||
position: relative;
|
||||
min-width: 80px;
|
||||
|
||||
> button {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--chosen-gradient);
|
||||
}
|
||||
|
||||
.text-label {
|
||||
background: var(--color-5-dullgray);
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
height: 12px;
|
||||
border-radius: 0 0 0 2px;
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined > button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: inset 0 0 0 1px rgba(var(--color-5-dullgray-rgb), var(--outline-amount));
|
||||
}
|
||||
|
||||
&.transparency > button {
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
background-size: var(--color-transparent-checkered-background-size);
|
||||
background-position: var(--color-transparent-checkered-background-position);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
&:not(.disabled).none > button {
|
||||
background: var(--color-none);
|
||||
background-repeat: var(--color-none-repeat);
|
||||
background-position: var(--color-none-position);
|
||||
background-size: var(--color-none-size-24px);
|
||||
background-image: var(--color-none-image-24px);
|
||||
}
|
||||
|
||||
&.disabled.none > button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-4-dimgray);
|
||||
}
|
||||
|
||||
&:not(.disabled):hover > button .text-label,
|
||||
&:not(.disabled).open > button .text-label {
|
||||
background: var(--color-6-lowergray);
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
|
||||
&.disabled > button .text-label {
|
||||
background: var(--color-4-dimgray);
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
|
||||
> .floating-menu {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
|
|
@ -65,6 +66,12 @@
|
|||
export function element(): HTMLInputElement | HTMLTextAreaElement | undefined {
|
||||
return inputOrTextarea;
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
dispatch("textChangeCanceled");
|
||||
|
||||
if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
|
||||
|
|
@ -83,7 +90,7 @@
|
|||
on:blur={() => dispatch("textChanged")}
|
||||
on:change={() => dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Enter" && dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Escape" && dispatch("textChangeCanceled")}
|
||||
on:keydown={(e) => e.key === "Escape" && cancel()}
|
||||
on:pointerdown
|
||||
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
data-input-element
|
||||
|
|
@ -102,7 +109,7 @@
|
|||
on:blur={() => dispatch("textChanged")}
|
||||
on:change={() => dispatch("textChanged")}
|
||||
on:keydown={(e) => (macKeyboardLayout ? e.metaKey : e.ctrlKey) && e.key === "Enter" && dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Escape" && dispatch("textChangeCanceled")}
|
||||
on:keydown={(e) => e.key === "Escape" && cancel()}
|
||||
on:pointerdown
|
||||
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/messages";
|
||||
import { evaluateMathExpression } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
const BUTTONS_LEFT = 0b0000_0001;
|
||||
|
|
@ -59,8 +61,10 @@
|
|||
let self: FieldInput | undefined;
|
||||
let inputRangeElement: HTMLInputElement | undefined;
|
||||
let text = displayText(value, unit);
|
||||
let isDragging = false;
|
||||
let editing = false;
|
||||
let isDragging = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
// Stays in sync with a binding to the actual input range slider element.
|
||||
let rangeSliderValue = value !== undefined ? value : 0;
|
||||
// Value used to render the position of the fake slider when applicable, and length of the progress colored region to the slider's left.
|
||||
|
|
@ -230,8 +234,8 @@
|
|||
// INCREMENT MODE: ARROW BUTTONS
|
||||
// =============================
|
||||
|
||||
function onIncrement(direction: "Decrease" | "Increase") {
|
||||
if (value === undefined) return;
|
||||
function onIncrementPointerDown(e: PointerEvent, direction: "Decrease" | "Increase") {
|
||||
if (value === undefined || e.button !== BUTTON_LEFT) return;
|
||||
|
||||
const actions: Record<NumberInputIncrementBehavior, () => void> = {
|
||||
Add: () => {
|
||||
|
|
@ -250,7 +254,41 @@
|
|||
},
|
||||
None: () => {},
|
||||
};
|
||||
actions[incrementBehavior]();
|
||||
|
||||
const sendAction = () => {
|
||||
if (!pressingArrow) return;
|
||||
|
||||
actions[incrementBehavior]();
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendAction, PRESS_REPEAT_INTERVAL_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
pressingArrow = true;
|
||||
initialValueBeforeDragging = value;
|
||||
let afterInitialDelay = false;
|
||||
sendAction();
|
||||
repeatTimeout = setTimeout(sendAction, PRESS_REPEAT_DELAY_MS);
|
||||
addEventListener("keydown", incrementPressAbort);
|
||||
}
|
||||
|
||||
function onIncrementPointerUp() {
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
}
|
||||
|
||||
function onIncrementMouseDown(e: MouseEvent) {
|
||||
if (e.button === BUTTON_RIGHT) incrementPressAbort();
|
||||
}
|
||||
|
||||
function incrementPressAbort() {
|
||||
const element = self?.element() || undefined;
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
updateValue(initialValueBeforeDragging);
|
||||
removeEventListener("keydown", onIncrementPointerUp);
|
||||
}
|
||||
|
||||
// =======================================
|
||||
|
|
@ -399,8 +437,8 @@
|
|||
if (rangeSliderClickDragState === "Aborted") {
|
||||
// If we've just aborted the drag by right clicking, but the user hasn't yet released the left mouse button, Firefox treats
|
||||
// some subsequent interactions with the slider (like that right mouse button release, or maybe mouse movement in some cases)
|
||||
// as input changes to the slider position. Thus, until we leave the "Aborted" state by releasing all mouse buttons,
|
||||
// we have to set the slider position back to currently intended value to fight against Firefox's attempts to let the user move it.
|
||||
// as input changes to the slider position. Thus, until we leave the "Aborted" state by releasing all mouse buttons, we have
|
||||
// to set the slider position back to the currently intended value to fight against Firefox's attempts to let the user move it.
|
||||
updateValue(rangeSliderValueAsRendered);
|
||||
|
||||
// Now we exit early because we're ignoring further user input until the user releases all mouse buttons, which gets us back to the "Ready" state.
|
||||
|
|
@ -511,10 +549,7 @@
|
|||
// Logic for aborting from pressing Escape.
|
||||
if (e instanceof KeyboardEvent) {
|
||||
// Detect if the user pressed Escape and abort the slider drag.
|
||||
if (e.key === "Escape") {
|
||||
// Call the abort helper function.
|
||||
sliderAbort();
|
||||
}
|
||||
if (e.key === "Escape") sliderAbort(true);
|
||||
}
|
||||
|
||||
// Logic for aborting from a right click.
|
||||
|
|
@ -522,7 +557,7 @@
|
|||
// This handler's "pointermove" event will be fired upon right click even if the cursor didn't move, which is why it's okay to check in this event handler.
|
||||
if (e instanceof PointerEvent && e.buttons & BUTTONS_RIGHT) {
|
||||
// Call the abort helper function
|
||||
sliderAbort();
|
||||
sliderAbort(false);
|
||||
}
|
||||
|
||||
// Recovery from the window losing focus while dragging the slider.
|
||||
|
|
@ -543,9 +578,11 @@
|
|||
// During this momentary step, the slider hasn't moved yet but we want to allow aborting from this limbo state.
|
||||
function sliderAbortFromMousedown(e: MouseEvent | KeyboardEvent) {
|
||||
// Logic for aborting from a right click or pressing Escape.
|
||||
if ((e instanceof KeyboardEvent && e.key === "Escape") || (e instanceof MouseEvent && e.button === BUTTON_RIGHT)) {
|
||||
const abortWithEscape = e instanceof KeyboardEvent && e.key === "Escape";
|
||||
const abortWithRightClick = e instanceof MouseEvent && e.button === BUTTON_RIGHT;
|
||||
if (abortWithEscape || abortWithRightClick) {
|
||||
// Call the abort helper function
|
||||
sliderAbort();
|
||||
sliderAbort(abortWithEscape);
|
||||
|
||||
// Clean up these event listeners because they were for getting us into this function and now we're done with them.
|
||||
removeEventListener("mousedown", sliderAbortFromMousedown);
|
||||
|
|
@ -554,7 +591,10 @@
|
|||
}
|
||||
|
||||
// Helper function that performs the state management and cleanup for aborting the slider drag.
|
||||
function sliderAbort() {
|
||||
function sliderAbort(abortWithEscape: boolean) {
|
||||
const element = self?.element() || undefined;
|
||||
if (abortWithEscape && element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
// End the user's drag by instantaneously disabling and re-enabling the range input element
|
||||
if (inputRangeElement) inputRangeElement.disabled = true;
|
||||
setTimeout(() => {
|
||||
|
|
@ -580,9 +620,7 @@
|
|||
// dragging the slider, hitting Escape, then releasing the mouse button. This results in being transferred by `onSliderInput()` to the
|
||||
// "Deciding" state when we should remain in the "Ready" state as set here. (For debugging, this can be visualized in CSS by
|
||||
// recoloring the fake slider handle, which is shown in the "Deciding" state.)
|
||||
setTimeout(() => {
|
||||
rangeSliderClickDragState = "Ready";
|
||||
}, 0);
|
||||
setTimeout(() => (rangeSliderClickDragState = "Ready"), 0);
|
||||
|
||||
// Clean up the event listener that was used to call this function.
|
||||
removeEventListener("pointerup", sliderResetAbort);
|
||||
|
|
@ -617,8 +655,22 @@
|
|||
>
|
||||
{#if value !== undefined}
|
||||
{#if mode === "Increment" && incrementBehavior !== "None"}
|
||||
<button class="arrow left" on:click={() => onIncrement("Decrease")} tabindex="-1" />
|
||||
<button class="arrow right" on:click={() => onIncrement("Increase")} tabindex="-1" />
|
||||
<button
|
||||
class="arrow left"
|
||||
on:pointerdown={(e) => onIncrementPointerDown(e, "Decrease")}
|
||||
on:mousedown={onIncrementMouseDown}
|
||||
on:pointerup={onIncrementPointerUp}
|
||||
on:pointerleave={onIncrementPointerUp}
|
||||
tabindex="-1"
|
||||
></button>
|
||||
<button
|
||||
class="arrow right"
|
||||
on:pointerdown={(e) => onIncrementPointerDown(e, "Increase")}
|
||||
on:mousedown={onIncrementMouseDown}
|
||||
on:pointerup={onIncrementPointerUp}
|
||||
on:pointerleave={onIncrementPointerUp}
|
||||
tabindex="-1"
|
||||
></button>
|
||||
{/if}
|
||||
{#if mode === "Range"}
|
||||
<input
|
||||
|
|
@ -684,6 +736,8 @@
|
|||
border: none;
|
||||
border-radius: 2px;
|
||||
background: rgba(var(--color-1-nearblack-rgb), 0.5);
|
||||
// An outline can appear when pressing the arrow button with left click then hitting Escape, so this stops that from showing
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-4-dimgray);
|
||||
|
|
|
|||
|
|
@ -3,38 +3,46 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
// Linear Interpolation
|
||||
const lerp = (x: number, y: number, a: number): number => x * (1 - a) + y * a;
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS, PRESS_REPEAT_INTERVAL_RAPID_MS } from "@graphite/io-managers/input";
|
||||
|
||||
// Convert the position of the handle (0-1) to the position on the track (0-1).
|
||||
// This includes the 1/2 handle length gap of the possible handle positionson each side so the end of the handle doesn't go off the track.
|
||||
const handleToTrack = (handleLen: number, handlePos: number): number => lerp(handleLen / 2, 1 - handleLen / 2, handlePos);
|
||||
const ARROW_CLICK_DISTANCE = 0.05;
|
||||
const ARROW_REPEAT_DISTANCE = 0.01;
|
||||
|
||||
const pointerPosition = (direction: ScrollbarDirection, e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
// Convert the position of the thumb (0-1) to the position on the track (0-1).
|
||||
// This includes the 1/2 thumb length gap of the possible thumb position each side so the end of the thumb doesn't go off the track.
|
||||
const lerp = (a: number, b: number, t: number): number => a * (1 - t) + b * t;
|
||||
const thumbToTrack = (thumbLength: number, thumbPosition: number): number => lerp(thumbLength / 2, 1 - thumbLength / 2, thumbPosition);
|
||||
|
||||
const dispatch = createEventDispatcher<{ handlePosition: number; pressTrack: number; pointerup: undefined }>();
|
||||
const pointerPosition = (e: PointerEvent): number => (direction === "Vertical" ? e.clientY : e.clientX);
|
||||
|
||||
const clamp01 = (value: number): number => Math.min(Math.max(value, 0), 1);
|
||||
|
||||
const dispatch = createEventDispatcher<{ trackShift: number; thumbPosition: number; thumbDragStart: undefined; thumbDragEnd: undefined; thumbDragAbort: undefined }>();
|
||||
|
||||
export let direction: ScrollbarDirection = "Vertical";
|
||||
export let handlePosition = 0.5;
|
||||
export let handleLength = 0.5;
|
||||
export let thumbPosition = 0.5;
|
||||
export let thumbLength = 0.5;
|
||||
|
||||
let scrollTrack: HTMLDivElement | undefined;
|
||||
let dragging = false;
|
||||
let pointerPos = 0;
|
||||
let pressingTrack = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let pointerPositionLastFrame = 0;
|
||||
let thumbTop: string | undefined = undefined;
|
||||
let thumbBottom: string | undefined = undefined;
|
||||
let thumbLeft: string | undefined = undefined;
|
||||
let thumbRight: string | undefined = undefined;
|
||||
|
||||
$: start = handleToTrack(handleLength, handlePosition) - handleLength / 2;
|
||||
$: end = 1 - handleToTrack(handleLength, handlePosition) - handleLength / 2;
|
||||
$: start = thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: end = 1 - thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: [thumbTop, thumbBottom, thumbLeft, thumbRight] = direction === "Vertical" ? [`${start * 100}%`, `${end * 100}%`, "0%", "0%"] : ["0%", "0%", `${start * 100}%`, `${end * 100}%`];
|
||||
|
||||
function trackLength(): number | undefined {
|
||||
if (scrollTrack === undefined) return undefined;
|
||||
return direction === "Vertical" ? scrollTrack.clientHeight - handleLength : scrollTrack.clientWidth;
|
||||
return direction === "Vertical" ? scrollTrack.clientHeight - thumbLength : scrollTrack.clientWidth;
|
||||
}
|
||||
|
||||
function trackOffset(): number | undefined {
|
||||
|
|
@ -42,72 +50,168 @@
|
|||
return direction === "Vertical" ? scrollTrack.getBoundingClientRect().top : scrollTrack.getBoundingClientRect().left;
|
||||
}
|
||||
|
||||
function clampHandlePosition(newPos: number) {
|
||||
const clampedPosition = Math.min(Math.max(newPos, 0), 1);
|
||||
dispatch("handlePosition", clampedPosition);
|
||||
function dragThumb(e: PointerEvent) {
|
||||
if (dragging) return;
|
||||
|
||||
dragging = true;
|
||||
dispatch("thumbDragStart");
|
||||
pointerPositionLastFrame = pointerPosition(e);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function updateHandlePosition(e: PointerEvent) {
|
||||
function pressArrow(direction: number) {
|
||||
const sendMove = () => {
|
||||
if (!pressingArrow) return;
|
||||
|
||||
const distance = afterInitialDelay ? ARROW_REPEAT_DISTANCE : ARROW_CLICK_DISTANCE;
|
||||
dispatch("trackShift", -direction * distance);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_RAPID_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
pressingArrow = true;
|
||||
dispatch("thumbDragStart");
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function pressTrack(e: PointerEvent) {
|
||||
if (dragging) return;
|
||||
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
const offset = trackOffset();
|
||||
if (length === undefined || offset === undefined) return;
|
||||
|
||||
const position = pointerPosition(direction, e);
|
||||
const sendMove = () => {
|
||||
if (!pressingTrack) return;
|
||||
|
||||
clampHandlePosition(handlePosition + (position - pointerPos) / (length * (1 - handleLength)));
|
||||
pointerPos = position;
|
||||
const oldPointer = thumbToTrack(thumbLength, thumbPosition) * length + offset;
|
||||
const newPointer = pointerPosition(e);
|
||||
|
||||
// Check if the thumb has reached the cursor position
|
||||
const proposedThumbPosition = (newPointer - offset) / length;
|
||||
if (proposedThumbPosition >= start && proposedThumbPosition <= 1 - end) {
|
||||
// End pressing the track
|
||||
pressingTrack = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
|
||||
// Begin dragging the thumb
|
||||
dragging = true;
|
||||
pointerPositionLastFrame = newPointer;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const move = newPointer - oldPointer < 0 ? 1 : -1;
|
||||
dispatch("trackShift", move);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
dispatch("thumbDragStart");
|
||||
pressingTrack = true;
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function grabHandle(e: PointerEvent) {
|
||||
if (!dragging) {
|
||||
dragging = true;
|
||||
pointerPos = pointerPosition(direction, e);
|
||||
function abortInteraction() {
|
||||
if (pressingTrack || pressingArrow) {
|
||||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
dispatch("thumbDragAbort");
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
dispatch("thumbDragAbort");
|
||||
}
|
||||
}
|
||||
|
||||
function grabArea(e: PointerEvent) {
|
||||
if (!dragging) {
|
||||
const length = trackLength();
|
||||
const offset = trackOffset();
|
||||
if (length === undefined || offset === undefined) return;
|
||||
function onPointerUp() {
|
||||
if (dragging) dispatch("thumbDragEnd");
|
||||
|
||||
const oldPointer = handleToTrack(handleLength, handlePosition) * length + offset;
|
||||
const pointerPos = pointerPosition(direction, e);
|
||||
dispatch("pressTrack", pointerPos - oldPointer);
|
||||
}
|
||||
}
|
||||
|
||||
function pointerUp() {
|
||||
dragging = false;
|
||||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function pointerMove(e: PointerEvent) {
|
||||
if (dragging) updateHandlePosition(e);
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (pressingTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pressingArrow) {
|
||||
const target = e.target || undefined;
|
||||
if (!target || !(target instanceof Element)) return;
|
||||
if (!target?.closest?.("[data-scrollbar-arrow]")) {
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
|
||||
const positionPositionThisFrame = pointerPosition(e);
|
||||
const dragDelta = positionPositionThisFrame - pointerPositionLastFrame;
|
||||
const movement = dragDelta / (length * (1 - thumbLength));
|
||||
const newThumbPosition = clamp01(thumbPosition + movement);
|
||||
dispatch("thumbPosition", newThumbPosition);
|
||||
|
||||
pointerPositionLastFrame = positionPositionThisFrame;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
removeEvents();
|
||||
}
|
||||
|
||||
function changePosition(difference: number) {
|
||||
const length = trackLength();
|
||||
if (length === undefined) return;
|
||||
|
||||
clampHandlePosition(handlePosition + difference / length);
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortInteraction();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("pointerup", pointerUp);
|
||||
window.addEventListener("pointermove", pointerMove);
|
||||
});
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") abortInteraction();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("pointerup", pointerUp);
|
||||
window.removeEventListener("pointermove", pointerMove);
|
||||
});
|
||||
function addEvents() {
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
window.removeEventListener("pointermove", onPointerMove);
|
||||
window.removeEventListener("mousedown", onMouseDown);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`} on:pointerup={() => dispatch("pointerup")}>
|
||||
<button class="arrow decrease" on:pointerdown={() => changePosition(-50)} tabindex="-1" />
|
||||
<div class="scroll-track" bind:this={scrollTrack} on:pointerdown={grabArea}>
|
||||
<div class="scroll-thumb" on:pointerdown={grabHandle} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
|
||||
<button class="arrow decrease" on:pointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
<div class="scroll-track" on:pointerdown={pressTrack} bind:this={scrollTrack}>
|
||||
<div class="scroll-thumb" on:pointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
</div>
|
||||
<button class="arrow increase" on:click={() => changePosition(50)} tabindex="-1" />
|
||||
<button class="arrow increase" on:pointerdown={() => pressArrow(1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
@ -115,20 +219,66 @@
|
|||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
--arrow-color: var(--color-5-dullgray);
|
||||
flex: 0 0 auto;
|
||||
background: none;
|
||||
border: none;
|
||||
border-style: solid;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&:hover {
|
||||
--arrow-color: var(--color-6-lowergray);
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
--arrow-color: var(--color-c-brightgray);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical .arrow.decrease::after {
|
||||
margin: 4px 3px;
|
||||
border-width: 0 5px 8px 5px;
|
||||
border-color: transparent transparent var(--arrow-color) transparent;
|
||||
}
|
||||
|
||||
&.vertical .arrow.increase::after {
|
||||
margin: 4px 3px;
|
||||
border-width: 8px 5px 0 5px;
|
||||
border-color: var(--arrow-color) transparent transparent transparent;
|
||||
}
|
||||
|
||||
&.horizontal .arrow.decrease::after {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 8px 5px 0;
|
||||
border-color: transparent var(--arrow-color) transparent transparent;
|
||||
}
|
||||
|
||||
&.horizontal .arrow.increase::after {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent var(--arrow-color);
|
||||
}
|
||||
|
||||
.scroll-track {
|
||||
flex: 1 1 100%;
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
|
||||
.scroll-thumb {
|
||||
position: absolute;
|
||||
|
|
@ -140,70 +290,6 @@
|
|||
background: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-click-area {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
|
||||
.arrow.decrease {
|
||||
margin: 4px 3px;
|
||||
border-width: 0 5px 8px 5px;
|
||||
border-color: transparent transparent var(--color-5-dullgray) transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent transparent var(--color-6-lowergray) transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent transparent var(--color-c-brightgray) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow.increase {
|
||||
margin: 4px 3px;
|
||||
border-width: 8px 5px 0 5px;
|
||||
border-color: var(--color-5-dullgray) transparent transparent transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-6-lowergray) transparent transparent transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: var(--color-c-brightgray) transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
|
||||
.arrow.decrease {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 8px 5px 0;
|
||||
border-color: transparent var(--color-5-dullgray) transparent transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: transparent var(--color-6-lowergray) transparent transparent;
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent var(--color-c-brightgray) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow.increase {
|
||||
margin: 3px 4px;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent var(--color-5-dullgray);
|
||||
|
||||
&:hover {
|
||||
border-color: transparent transparent transparent var(--color-6-lowergray);
|
||||
}
|
||||
&:active {
|
||||
border-color: transparent transparent transparent var(--color-c-brightgray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,21 +3,27 @@
|
|||
|
||||
import { Color, type Gradient } from "@graphite/messages";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient }>();
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
// export let disabled = false;
|
||||
// export let tooltip: string | undefined = undefined;
|
||||
|
||||
let markerTrack: LayoutRow | undefined;
|
||||
let markerTrack: LayoutRow | undefined = undefined;
|
||||
let positionRestore: number | undefined = undefined;
|
||||
let deletionRestore: boolean | undefined = undefined;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === 0) {
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
activeMarkerIndex = index;
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
addEvents();
|
||||
|
|
@ -25,7 +31,7 @@
|
|||
}
|
||||
|
||||
// Right-click to delete
|
||||
if (e.button === 2) {
|
||||
if (e.button === BUTTON_RIGHT && deletionRestore === undefined) {
|
||||
deleteStopByIndex(index);
|
||||
return;
|
||||
}
|
||||
|
|
@ -41,6 +47,8 @@
|
|||
}
|
||||
|
||||
function insertStop(e: MouseEvent) {
|
||||
if (e.button !== BUTTON_LEFT) return;
|
||||
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
|
|
@ -62,14 +70,20 @@
|
|||
|
||||
gradient.stops.splice(index, 0, { position, color });
|
||||
activeMarkerIndex = index;
|
||||
deletionRestore = true;
|
||||
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
dispatch("gradient", gradient);
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
||||
function deleteStop(e: KeyboardEvent) {
|
||||
if (e.key.toLowerCase() !== "delete" && e.key.toLowerCase() !== "backspace") return;
|
||||
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (positionRestore !== undefined) stopDrag();
|
||||
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +96,7 @@
|
|||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||
}
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("activeMarkerIndexChange", activeMarkerIndex);
|
||||
dispatch("gradient", gradient);
|
||||
|
|
@ -89,10 +104,18 @@
|
|||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
// Just in case the mouseup event is lost
|
||||
if (e.buttons === 0) removeEvents();
|
||||
if (e.buttons === 0) stopDrag();
|
||||
|
||||
let position = markerPosition(e);
|
||||
if (position === undefined) return;
|
||||
|
||||
if (positionRestore === undefined) positionRestore = position;
|
||||
if (deletionRestore === undefined) {
|
||||
deletionRestore = false;
|
||||
|
||||
dispatch("dragging", true);
|
||||
}
|
||||
|
||||
setPosition(index, position);
|
||||
}
|
||||
|
||||
|
|
@ -107,24 +130,61 @@
|
|||
dispatch("gradient", gradient);
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (activeMarkerIndex !== undefined) {
|
||||
moveMarker(e, activeMarkerIndex);
|
||||
function abortDrag() {
|
||||
if (activeMarkerIndex === undefined) return;
|
||||
|
||||
if (deletionRestore) {
|
||||
deleteStopByIndex(activeMarkerIndex);
|
||||
} else if (positionRestore !== undefined) {
|
||||
setPosition(activeMarkerIndex, positionRestore);
|
||||
}
|
||||
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
removeEvents();
|
||||
|
||||
positionRestore = undefined;
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("dragging", false);
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (activeMarkerIndex !== undefined) moveMarker(e, activeMarkerIndex);
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
removeEvents();
|
||||
stopDrag();
|
||||
}
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortDrag();
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
const element = markerTrack?.div();
|
||||
if (element) preventEscapeClosingParentFloatingMenu(element);
|
||||
|
||||
abortDrag();
|
||||
}
|
||||
}
|
||||
|
||||
function addEvents() {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", deleteStop);
|
||||
|
|
@ -158,24 +218,22 @@
|
|||
"--gradient-stops": gradient.toLinearGradientCSS(),
|
||||
}}
|
||||
>
|
||||
<LayoutRow class="gradient-strip" on:click={insertStop}></LayoutRow>
|
||||
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
||||
<LayoutRow class="marker-track" bind:this={markerTrack}>
|
||||
{#each gradient.stops as marker, index}
|
||||
<svg
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
class="marker"
|
||||
class:active={index === activeMarkerIndex}
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
data-gradient-marker
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path class="inner-fill" d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z" />
|
||||
<path
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
d="M10,11.5H2c-0.8,0-1.5-0.7-1.5-1.5V6.8c0-0.4,0.2-0.8,0.4-1.1L6,0.7l5.1,5.1c0.3,0.3,0.4,0.7,0.4,1.1V10C11.5,10.8,10.8,11.5,10,11.5z"
|
||||
/>
|
||||
<path
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
class="outer-border"
|
||||
d="M6,1.4L1.3,6.1C1.1,6.3,1,6.6,1,6.8V10c0,0.6,0.4,1,1,1h8c0.6,0,1-0.4,1-1V6.8c0-0.3-0.1-0.5-0.3-0.7L6,1.4M6,0l5.4,5.4C11.8,5.8,12,6.3,12,6.8V10c0,1.1-0.9,2-2,2H2c-1.1,0-2-0.9-2-2V6.8c0-0.5,0.2-1,0.6-1.4L6,0z"
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -216,6 +274,7 @@
|
|||
margin-left: var(--marker-half-width);
|
||||
width: calc(100% - 2 * var(--marker-half-width));
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
|
||||
.marker {
|
||||
position: absolute;
|
||||
|
|
@ -223,30 +282,40 @@
|
|||
left: calc(var(--marker-position) * 100%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
pointer-events: auto;
|
||||
overflow: visible;
|
||||
padding-top: 12px;
|
||||
margin-top: -12px;
|
||||
|
||||
// Inner fill
|
||||
path:first-child {
|
||||
.inner-fill {
|
||||
fill: var(--marker-color);
|
||||
}
|
||||
|
||||
// Outer border
|
||||
path:last-child {
|
||||
.outer-border {
|
||||
fill: var(--color-5-dullgray);
|
||||
}
|
||||
|
||||
&:not(.active) path:first-child:hover + path:last-child,
|
||||
&:not(.active) path:last-child:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
&:not(.active) {
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
// Outer border when active
|
||||
&.active path:last-child {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
&.active {
|
||||
.inner-fill {
|
||||
filter: drop-shadow(0 0 1px var(--color-2-mildblack)) drop-shadow(0 0 1px var(--color-2-mildblack));
|
||||
}
|
||||
|
||||
&.active path:first-child:hover + path:last-child,
|
||||
&.active path:last-child:hover {
|
||||
fill: var(--color-f-white);
|
||||
// Outer border when active
|
||||
.outer-border {
|
||||
fill: var(--color-e-nearwhite);
|
||||
}
|
||||
|
||||
.inner-fill:hover + .outer-border,
|
||||
.outer-border:hover {
|
||||
fill: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<LayoutCol class="working-colors-button">
|
||||
<LayoutRow class="primary swatch">
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={primaryOpen}
|
||||
on:open={({ detail }) => (primaryOpen = detail)}
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="secondary swatch">
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0" />
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={secondaryOpen}
|
||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||
|
|
@ -70,27 +70,55 @@
|
|||
|
||||
> button {
|
||||
--swatch-color: #ffffff;
|
||||
--ring-color: var(--color-5-dullgray);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px var(--color-5-dullgray) solid;
|
||||
box-shadow: 0 0 0 2px var(--color-3-darkgray);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background);
|
||||
background-size:
|
||||
100% 100%,
|
||||
var(--color-transparent-checkered-background-size);
|
||||
background-position:
|
||||
0 0,
|
||||
var(--color-transparent-checkered-background-position);
|
||||
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
// Color of the panel background, used to extend outside the ring and appear to cut out a crescent from the lower circle (by covering it up with the panel background color)
|
||||
box-shadow: 0 0 0 2px var(--color-3-darkgray);
|
||||
background: var(--color-3-darkgray);
|
||||
|
||||
// Main color and checked transparency pattern (inset by 1px to begin inside/below the ring to avoid antialiasing artifacts)
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(var(--swatch-color), var(--swatch-color)), var(--color-transparent-checkered-background);
|
||||
background-size:
|
||||
100% 100%,
|
||||
var(--color-transparent-checkered-background-size);
|
||||
background-position:
|
||||
0 0,
|
||||
var(--color-transparent-checkered-background-position-plus-one);
|
||||
background-repeat: no-repeat, var(--color-transparent-checkered-background-repeat);
|
||||
}
|
||||
|
||||
// Gray ring outline
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2px var(--ring-color);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
border-color: var(--color-6-lowergray);
|
||||
--ring-color: var(--color-6-lowergray);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,14 @@
|
|||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
$: iconSizeClass = ((icon: IconName) => {
|
||||
return `size-${iconSizeOverride || ICONS[icon].size}`;
|
||||
const iconData = ICONS[icon];
|
||||
// eslint-disable-next-line no-console
|
||||
if (!iconData) {
|
||||
console.warn(`Icon "${icon}" does not exist.`);
|
||||
return "size-24";
|
||||
}
|
||||
if (iconData.size === undefined) return "";
|
||||
return `size-${iconSizeOverride || iconData.size}`;
|
||||
})(icon);
|
||||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
|
|
@ -20,7 +27,7 @@
|
|||
</script>
|
||||
|
||||
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>
|
||||
{@html ICON_SVG_STRINGS[icon]}
|
||||
{@html ICON_SVG_STRINGS[icon] || "<22>"}
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@
|
|||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
|
||||
|
||||
const BUTTON_MIDDLE = 1;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
export let tabMinWidths = false;
|
||||
|
|
@ -100,7 +102,7 @@
|
|||
}}
|
||||
on:auxclick={(e) => {
|
||||
// Middle mouse button click
|
||||
if (e.button === 1) {
|
||||
if (e.button === BUTTON_MIDDLE) {
|
||||
e.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
|
|
@ -110,7 +112,7 @@
|
|||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
|
||||
// The downside of using mouseup is that the mousedown didn't have to originate in the same element.
|
||||
// A possible future improvement could save the target element during mousedown and check if it's the same here.
|
||||
if (!isEventSupported("auxclick") && e.button === 1) {
|
||||
if (!isEventSupported("auxclick") && e.button === BUTTON_MIDDLE) {
|
||||
e.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@
|
|||
const MIN_PANEL_SIZE = 100;
|
||||
const PANEL_SIZES = {
|
||||
/**/ root: 100,
|
||||
/* ├── */ content: 80,
|
||||
/* │ ├── */ document: 100,
|
||||
/* └── */ details: 20,
|
||||
/* ├── */ properties: 45,
|
||||
/* └── */ layers: 55,
|
||||
/* ├─ */ content: 80,
|
||||
/* │ └─ */ document: 100,
|
||||
/* └─ */ details: 20,
|
||||
/* ├─ */ properties: 45,
|
||||
/* └─ */ layers: 55,
|
||||
};
|
||||
|
||||
let panelSizes = PANEL_SIZES;
|
||||
let documentPanel: Panel | undefined;
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
|
||||
$: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
|
||||
|
||||
|
|
@ -64,32 +66,68 @@
|
|||
const proportionBeingResized = totalResizingSpaceOccupied / parentElementSize;
|
||||
|
||||
// Prevent cursor flicker as mouse temporarily leaves the gutter
|
||||
gutter.setPointerCapture(e.pointerId);
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
const mouseStart = isHorizontal ? e.clientX : e.clientY;
|
||||
|
||||
const updatePosition = (e: PointerEvent) => {
|
||||
const abortResize = () => {
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
|
||||
if (gutterResizeRestore !== undefined) {
|
||||
panelSizes[nextSiblingName] = gutterResizeRestore[0];
|
||||
panelSizes[prevSiblingName] = gutterResizeRestore[1];
|
||||
gutterResizeRestore = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const mouseCurrent = isHorizontal ? e.clientX : e.clientY;
|
||||
let mouseDelta = mouseStart - mouseCurrent;
|
||||
|
||||
mouseDelta = Math.max(nextSiblingSize + mouseDelta, MIN_PANEL_SIZE) - nextSiblingSize;
|
||||
mouseDelta = prevSiblingSize - Math.max(prevSiblingSize - mouseDelta, MIN_PANEL_SIZE);
|
||||
|
||||
if (gutterResizeRestore === undefined) gutterResizeRestore = [panelSizes[nextSiblingName], panelSizes[prevSiblingName]];
|
||||
|
||||
panelSizes[nextSiblingName] = ((nextSiblingSize + mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
|
||||
panelSizes[prevSiblingName] = ((prevSiblingSize - mouseDelta) / totalResizingSpaceOccupied) * proportionBeingResized * 100;
|
||||
};
|
||||
|
||||
const cleanup = (e: PointerEvent) => {
|
||||
gutter.releasePointerCapture(e.pointerId);
|
||||
|
||||
document.removeEventListener("pointermove", updatePosition);
|
||||
document.removeEventListener("pointerleave", cleanup);
|
||||
document.removeEventListener("pointerup", cleanup);
|
||||
const onPointerUp = () => {
|
||||
gutterResizeRestore = undefined;
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", updatePosition);
|
||||
document.addEventListener("pointerleave", cleanup);
|
||||
document.addEventListener("pointerup", cleanup);
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
if (e.buttons & BUTTONS_RIGHT) abortResize();
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") abortResize();
|
||||
};
|
||||
|
||||
const addListeners = () => {
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
const removeListeners = () => {
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
|
||||
addListeners();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,16 @@ import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
|||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";
|
||||
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_MIDDLE = 1;
|
||||
const BUTTON_RIGHT = 2;
|
||||
const BUTTON_BACK = 3;
|
||||
const BUTTON_FORWARD = 4;
|
||||
|
||||
export const PRESS_REPEAT_DELAY_MS = 400;
|
||||
export const PRESS_REPEAT_INTERVAL_MS = 72;
|
||||
export const PRESS_REPEAT_INTERVAL_RAPID_MS = 10;
|
||||
|
||||
type EventName = keyof HTMLElementEventMap | keyof WindowEventHandlersEventMap | "modifyinputfield";
|
||||
type EventListenerTarget = {
|
||||
addEventListener: typeof window.addEventListener;
|
||||
|
|
@ -155,7 +165,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
// Block middle mouse button auto-scroll mode (the circlar gizmo that appears and allows quick scrolling by moving the cursor above or below it)
|
||||
if (e.button === 1) e.preventDefault();
|
||||
if (e.button === BUTTON_MIDDLE) e.preventDefault();
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
|
|
@ -172,7 +182,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
}
|
||||
|
||||
if (!inTextInput && !inContextMenu) {
|
||||
const isLeftOrRightClick = e.button === 2 || e.button === 0;
|
||||
const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT;
|
||||
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick);
|
||||
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
||||
}
|
||||
|
|
@ -188,7 +198,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
// TODO: This works in Chrome but not in Firefox
|
||||
// TODO: Possible workaround: use the browser's history API to block navigation:
|
||||
// TODO: <https://stackoverflow.com/questions/57102502/preventing-mouse-fourth-and-fifth-buttons-from-navigating-back-forward-in-browse>
|
||||
if (e.button === 3 || e.button === 4) e.preventDefault();
|
||||
if (e.button === BUTTON_BACK || e.button === BUTTON_FORWARD) e.preventDefault();
|
||||
|
||||
if (!e.buttons) viewportPointerInteractionOngoing = false;
|
||||
|
||||
|
|
@ -206,11 +216,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
|
||||
// `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead
|
||||
let buttons = 1;
|
||||
if (e.button === 0) buttons = 1; // Left
|
||||
if (e.button === 2) buttons = 2; // Right
|
||||
if (e.button === 1) buttons = 4; // Middle
|
||||
if (e.button === 3) buttons = 8; // Back
|
||||
if (e.button === 4) buttons = 16; // Forward
|
||||
if (e.button === BUTTON_LEFT) buttons = 1; // Left
|
||||
if (e.button === BUTTON_RIGHT) buttons = 2; // Right
|
||||
if (e.button === BUTTON_MIDDLE) buttons = 4; // Middle
|
||||
if (e.button === BUTTON_BACK) buttons = 8; // Back
|
||||
if (e.button === BUTTON_FORWARD) buttons = 16; // Forward
|
||||
|
||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||
editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
|
||||
|
|
|
|||
|
|
@ -667,8 +667,8 @@ export class Color {
|
|||
return new Color(this.red, this.green, this.blue, 1);
|
||||
}
|
||||
|
||||
contrastingColor(): "black" | "white" {
|
||||
if (this.none) return "black";
|
||||
luminance(): number | undefined {
|
||||
if (this.none) return undefined;
|
||||
|
||||
// Convert alpha into white
|
||||
const r = this.red * this.alpha + (1 - this.alpha);
|
||||
|
|
@ -681,9 +681,15 @@ export class Color {
|
|||
const linearG = g <= 0.04045 ? g / 12.92 : ((g + 0.055) / 1.055) ** 2.4;
|
||||
const linearB = b <= 0.04045 ? b / 12.92 : ((b + 0.055) / 1.055) ** 2.4;
|
||||
|
||||
const linear = linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||
}
|
||||
|
||||
return linear > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
contrastingColor(): "black" | "white" {
|
||||
if (this.none) return "black";
|
||||
|
||||
const luminance = this.luminance();
|
||||
|
||||
return luminance && luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -944,7 +950,7 @@ export class CheckboxInput extends WidgetProps {
|
|||
tooltip!: string | undefined;
|
||||
}
|
||||
|
||||
export class ColorButton extends WidgetProps {
|
||||
export class ColorInput extends WidgetProps {
|
||||
@Transform(({ value }) => {
|
||||
if (value instanceof Gradient) return value;
|
||||
const gradient = value["Gradient"];
|
||||
|
|
@ -978,6 +984,37 @@ export class ColorButton extends WidgetProps {
|
|||
|
||||
export type FillChoice = Color | Gradient;
|
||||
|
||||
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number {
|
||||
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
|
||||
const [range1, range2] = pair.map((color) => Color.fromCSS(window.getComputedStyle(document.body).getPropertyValue(color)) || new Color("none"));
|
||||
|
||||
const contrast = (color: Color): number => {
|
||||
const colorLuminance = color.luminance() || 0;
|
||||
let rangeLuminance1 = range1.luminance() || 0;
|
||||
let rangeLuminance2 = range2.luminance() || 0;
|
||||
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
||||
|
||||
const distance = (() => {
|
||||
if (colorLuminance < rangeLuminance1) return rangeLuminance1 - colorLuminance;
|
||||
if (colorLuminance > rangeLuminance2) return colorLuminance - rangeLuminance2;
|
||||
return 0;
|
||||
})();
|
||||
|
||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - (color.toHSV()?.s || 0));
|
||||
};
|
||||
|
||||
if (value instanceof Gradient) {
|
||||
if (value.stops.length === 0) return 0;
|
||||
|
||||
const first = contrast(value.stops[0].color);
|
||||
const last = contrast(value.stops[value.stops.length - 1].color);
|
||||
|
||||
return Math.min(first, last);
|
||||
}
|
||||
|
||||
return contrast(value);
|
||||
}
|
||||
|
||||
type MenuEntryCommon = {
|
||||
label: string;
|
||||
icon?: IconName;
|
||||
|
|
@ -1314,7 +1351,7 @@ export class PivotInput extends WidgetProps {
|
|||
const widgetSubTypes = [
|
||||
{ value: BreadcrumbTrailButtons, name: "BreadcrumbTrailButtons" },
|
||||
{ value: CheckboxInput, name: "CheckboxInput" },
|
||||
{ value: ColorButton, name: "ColorButton" },
|
||||
{ value: ColorInput, name: "ColorInput" },
|
||||
{ value: CurveInput, name: "CurveInput" },
|
||||
{ value: DropdownInput, name: "DropdownInput" },
|
||||
{ value: FontInput, name: "FontInput" },
|
||||
|
|
|
|||
|
|
@ -557,16 +557,29 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Translates document (in viewport coords)
|
||||
#[wasm_bindgen(js_name = panCanvasAbortPrepare)]
|
||||
pub fn pan_canvas_abort_prepare(&self, x_not_y_axis: bool) {
|
||||
let message = NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = panCanvasAbort)]
|
||||
pub fn pan_canvas_abort(&self, x_not_y_axis: bool) {
|
||||
let message = NavigationMessage::CanvasPanAbort { x_not_y_axis };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Translates document (in viewport coords)
|
||||
#[wasm_bindgen(js_name = panCanvas)]
|
||||
pub fn translate_canvas(&self, delta_x: f64, delta_y: f64) {
|
||||
pub fn pan_canvas(&self, delta_x: f64, delta_y: f64) {
|
||||
let message = NavigationMessage::CanvasPan { delta: (delta_x, delta_y).into() };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Translates document (in viewport coords)
|
||||
#[wasm_bindgen(js_name = panCanvasByFraction)]
|
||||
pub fn translate_canvas_by_fraction(&self, delta_x: f64, delta_y: f64) {
|
||||
pub fn pan_canvas_by_fraction(&self, delta_x: f64, delta_y: f64) {
|
||||
let message = NavigationMessage::CanvasPanByViewportFraction { delta: (delta_x, delta_y).into() };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue