mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 14:26:49 -06:00
M6: plugin widgets have hover styles (gap 6) (#1634)
This commit is contained in:
@@ -11,7 +11,7 @@ pub mod widget;
|
||||
|
||||
pub use manifest::{API_VERSION, Access, MANIFEST_FILE, Manifest, Panel, Param, ParamKind, Permissions, PluginCommand};
|
||||
pub use sandbox::{ENTRY, Host, IMPORT_MODULE, Instance, Limits, PluginError, RunOutput, UiChange, check_module, run};
|
||||
pub use widget::{MenuItem, MenuKind, WidgetKind, WidgetNode, WidgetRender};
|
||||
pub use widget::{MenuItem, MenuKind, WidgetHover, WidgetKind, WidgetNode, WidgetRender};
|
||||
|
||||
#[cfg(test)]
|
||||
mod bundle_tests;
|
||||
|
||||
@@ -118,10 +118,54 @@ pub struct WidgetNode {
|
||||
/// An image layer's bytes, base64.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub data: String,
|
||||
/// How the layer looks while the pointer is over it (hosts that don't know it draw the layer
|
||||
/// as it is).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub hover: Option<WidgetHover>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub children: Vec<WidgetNode>,
|
||||
}
|
||||
|
||||
/// A widget layer's look while the pointer is over it: what it gives replaces the layer's own
|
||||
/// fill, stroke, opacity or (text) colour, as the reference tool's widgets' hover style does.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WidgetHover {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fill: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stroke: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opacity: Option<f64>,
|
||||
/// A text layer's colour.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
impl WidgetHover {
|
||||
/// Check and normalise it for a `kind` layer: hex colours, an opacity from 0 to 1, a colour for
|
||||
/// text and a fill for the rest, and something to show.
|
||||
pub fn check(&mut self, kind: WidgetKind) -> Result<(), String> {
|
||||
colour("hover fill", &mut self.fill)?;
|
||||
colour("hover stroke", &mut self.stroke)?;
|
||||
colour("hover color", &mut self.color)?;
|
||||
within("hover opacity", self.opacity, 0.0, 1.0)?;
|
||||
let text = kind == WidgetKind::Text;
|
||||
if self.color.is_some() && !text {
|
||||
return Err("`hover.color` is for text layers (others take `fill`)".into());
|
||||
}
|
||||
if self.fill.is_some() && text {
|
||||
return Err("a text layer's hover takes `color`, not `fill`".into());
|
||||
}
|
||||
if self.fill.is_some() && kind == WidgetKind::Image {
|
||||
return Err("an image layer's hover takes no `fill`".into());
|
||||
}
|
||||
if *self == WidgetHover::default() {
|
||||
return Err("`hover` needs a fill, stroke, opacity or color".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// What a widget callback hands back with `widget_render`: the widget's new state (`None`: keep
|
||||
/// it) and its layers, the root a frame.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -322,6 +366,9 @@ impl WidgetNode {
|
||||
only("text", !self.text.is_empty() || self.size.is_some() || self.color.is_some() || self.bold, kind == WidgetKind::Text)?;
|
||||
only("data", !self.data.is_empty(), kind == WidgetKind::Image)?;
|
||||
only("field", !self.field.is_empty(), kind == WidgetKind::Text)?;
|
||||
if let Some(h) = &mut self.hover {
|
||||
h.check(kind)?;
|
||||
}
|
||||
if self.text.chars().count() > MAX_TEXT {
|
||||
return Err(format!("a text is longer than {MAX_TEXT} characters"));
|
||||
}
|
||||
|
||||
@@ -158,3 +158,26 @@ fn field_values_are_cleaned_and_bounded() {
|
||||
assert!(clean_field(&"x".repeat(MAX_FIELD_CHARS + 1)).is_err());
|
||||
assert_eq!(clean_field("").unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_styles_are_checked_and_normalised() {
|
||||
let w = parse(json!({"root": {"type": "frame", "hover": {"opacity": 0.8}, "children": [
|
||||
{"type": "rectangle", "target": "go", "fill": "#0D99FF", "hover": {"fill": "0b7fd6", "stroke": "#000"}},
|
||||
{"type": "text", "text": "Go", "hover": {"color": "#fff"}}]}}))
|
||||
.unwrap();
|
||||
assert_eq!(w.root.hover.as_ref().and_then(|h| h.opacity), Some(0.8));
|
||||
let rect = w.root.children[0].hover.clone().unwrap();
|
||||
assert_eq!((rect.fill.as_deref(), rect.stroke.as_deref()), (Some("#0B7FD6"), Some("#000000")));
|
||||
assert_eq!(w.root.children[1].hover.as_ref().and_then(|h| h.color.as_deref()), Some("#FFFFFF"));
|
||||
for (bad, why) in [
|
||||
(json!({"type": "rectangle", "hover": {}}), "needs a fill"),
|
||||
(json!({"type": "rectangle", "hover": {"fill": "red"}}), "hex colour"),
|
||||
(json!({"type": "rectangle", "hover": {"opacity": 1.5}}), "from 0 to 1"),
|
||||
(json!({"type": "rectangle", "hover": {"color": "#fff"}}), "for text layers"),
|
||||
(json!({"type": "text", "text": "x", "hover": {"fill": "#fff"}}), "takes `color`"),
|
||||
] {
|
||||
let e = parse(json!({"root": {"type": "frame", "children": [bad]}})).unwrap_err();
|
||||
assert!(e.contains(why), "{e}");
|
||||
}
|
||||
assert!(!w.uses_api_4(), "a hover style needs no new API: older hosts draw the layer as it is");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user