use iced::widget::{container, slider, vertical_slider};
use iced::{Element, Theme, widget};
use serde_json::Value;
use crate::PlushieRenderer;
use crate::a11y::A11yOverrides;
use crate::iced_convert;
use crate::message::Message;
use crate::protocol::TreeNode;
use crate::registry::PlushieWidget;
use crate::render_ctx::RenderCtx;
use crate::widget::helpers::*;
use plushie_core::types::{Color, Length, PlushieType, Style as CoreStyle};
fn apply_rail_overrides(
style: &mut slider::Style,
rail_color: Option<iced::Color>,
rail_width: Option<f32>,
) {
if let Some(rc) = rail_color {
style.rail.backgrounds = (iced::Background::Color(rc), iced::Background::Color(rc));
}
if let Some(rw) = rail_width {
style.rail.width = rw;
}
}
fn handle_slider_message(
last_values: &mut std::collections::HashMap<(String, String), f64>,
msg: &Message,
) -> crate::registry::HandleResult {
use crate::registry::HandleResult;
match msg {
Message::Event {
window_id,
id,
value,
family,
} if family == "slide" => {
let v = value.as_f64().unwrap_or(0.0);
last_values.insert((window_id.clone(), id.clone()), v);
HandleResult::emit(vec![crate::protocol::OutgoingEvent::slide(id.clone(), v)])
}
Message::Event {
window_id,
id,
family,
..
} if family == "slide_release" => {
let key = (window_id.clone(), id.clone());
let v = last_values.remove(&key).unwrap_or(0.0);
HandleResult::emit(vec![crate::protocol::OutgoingEvent::slide_release(
id.clone(),
v,
)])
}
_ => HandleResult::Fallthrough,
}
}
fn effective_slider_step(step: Option<f64>, keyboard_step: Option<f64>) -> Option<f64> {
keyboard_step.or(step).map(|step| step.max(f64::EPSILON))
}
pub(crate) struct SliderWidget {
last_values: std::collections::HashMap<(String, String), f64>,
}
impl SliderWidget {
pub(crate) fn new() -> Self {
Self {
last_values: std::collections::HashMap::new(),
}
}
}
impl<R: PlushieRenderer> PlushieWidget<R> for SliderWidget {
fn type_names(&self) -> &[&str] {
&["slider"]
}
fn render<'a>(
&'a self,
node: &'a TreeNode,
ctx: &RenderCtx<'a, R>,
) -> Element<'a, Message, Theme, R> {
render_slider(node, *ctx)
}
fn handle_message(&mut self, msg: &Message) -> crate::registry::HandleResult {
handle_slider_message(&mut self.last_values, msg)
}
fn infer_a11y(&self, node: &TreeNode) -> Option<A11yOverrides> {
A11yOverrides::from_mnemonic_props(&node.props)
}
fn prune_stale(&mut self, live_ids: &std::collections::HashSet<(String, String)>) {
self.last_values.retain(|k, _| live_ids.contains(k));
}
fn fresh_for_session(&self) -> Box<dyn PlushieWidget<R>> {
Box::new(SliderWidget::new())
}
}
pub(crate) struct VerticalSliderWidget {
last_values: std::collections::HashMap<(String, String), f64>,
}
impl VerticalSliderWidget {
pub(crate) fn new() -> Self {
Self {
last_values: std::collections::HashMap::new(),
}
}
}
impl<R: PlushieRenderer> PlushieWidget<R> for VerticalSliderWidget {
fn type_names(&self) -> &[&str] {
&["vertical_slider"]
}
fn render<'a>(
&'a self,
node: &'a TreeNode,
ctx: &RenderCtx<'a, R>,
) -> Element<'a, Message, Theme, R> {
render_vertical_slider(node, *ctx)
}
fn handle_message(&mut self, msg: &Message) -> crate::registry::HandleResult {
handle_slider_message(&mut self.last_values, msg)
}
fn infer_a11y(&self, node: &TreeNode) -> Option<A11yOverrides> {
A11yOverrides::from_mnemonic_props(&node.props)
}
fn prune_stale(&mut self, live_ids: &std::collections::HashSet<(String, String)>) {
self.last_values.retain(|k, _| live_ids.contains(k));
}
fn fresh_for_session(&self) -> Box<dyn PlushieWidget<R>> {
Box::new(VerticalSliderWidget::new())
}
}
struct SliderProps {
value: Option<f64>,
step: Option<f64>,
keyboard_step: Option<f64>,
width: Option<Length>,
default: Option<f64>,
shift_step: Option<f64>,
label: Option<String>,
rail_color: Option<Color>,
style: Option<CoreStyle>,
}
impl SliderProps {
fn from_node(node: &TreeNode) -> Self {
let p = &node.props;
Self {
value: f64::extract(p, "value"),
step: f64::extract(p, "step"),
keyboard_step: f64::extract(p, "keyboard_step"),
width: Length::extract(p, "width"),
default: f64::extract(p, "default"),
shift_step: f64::extract(p, "shift_step"),
label: String::extract(p, "label"),
rail_color: Color::extract(p, "rail_color"),
style: CoreStyle::extract(p, "style"),
}
}
}
fn render_slider<'a, R: PlushieRenderer>(
node: &'a TreeNode,
ctx: RenderCtx<'a, R>,
) -> Element<'a, Message, Theme, R> {
let props = &node.props;
let sp = SliderProps::from_node(node);
let range = prop_range_f64(props);
let value = sp.value.unwrap_or(*range.start());
let width = sp
.width
.as_ref()
.map(iced_convert::length)
.unwrap_or(iced::Length::Fill);
let id = node.id.clone();
let release_id = node.id.clone();
let window_id = ctx.window_id.to_string();
let release_window_id = window_id.clone();
let mut s = slider(range, value, move |v| Message::Event {
window_id: window_id.clone(),
id: id.clone(),
value: serde_json::json!(v),
family: "slide".into(),
})
.on_release(Message::Event {
window_id: release_window_id,
id: release_id,
value: Value::Null,
family: "slide_release".into(),
})
.width(width);
if let Some(st) = effective_slider_step(sp.step, sp.keyboard_step) {
s = s.step(st);
}
if let Some(d) = sp.default {
s = s.default(d);
}
if let Some(h) = prop_animated_f32(&ctx.caches.interpolated_props, &node.id, props, "height") {
s = s.height(h);
}
if let Some(ss) = sp.shift_step {
s = s.shift_step(ss);
}
if let Some(label) = sp.label {
s = s.label(label);
}
let rail_color = sp.rail_color.as_ref().map(iced_convert::color);
let rail_width = prop_f32(props, "rail_width");
let has_rail_overrides = rail_color.is_some() || rail_width.is_some();
let circular = prop_bool_default(props, "circular_handle", false);
if circular {
let radius = prop_f32(props, "handle_radius").unwrap_or(8.0);
s = s.style(move |theme, status| {
let mut style = slider::default(theme, status).with_circular_handle(radius);
apply_rail_overrides(&mut style, rail_color, rail_width);
style
});
} else {
match &sp.style {
Some(CoreStyle::Preset(name)) => {
s = match name.as_str() {
"default" => {
if has_rail_overrides {
s.style(move |theme: &iced::Theme, status| {
let mut style = slider::default(theme, status);
apply_rail_overrides(&mut style, rail_color, rail_width);
style
})
} else {
s.style(slider::default)
}
}
_ => {
log::warn!(
"unknown style {:?} for widget type {:?}, using default",
name,
"slider"
);
s
}
};
}
Some(CoreStyle::Custom(style_map)) => {
let ov = style_overrides_from_style_map(&node.id, style_map, ctx.caches);
s = s.style(move |theme: &iced::Theme, status| {
let mut style = slider::default(theme, status);
apply_slider_handle_fields(&mut style.handle, &ov.base);
apply_rail_overrides(&mut style, rail_color, rail_width);
if matches!(status, slider::Status::Hovered) {
if let Some(ref f) = ov.hovered {
apply_slider_handle_fields(&mut style.handle, f);
} else {
style.handle.background =
deviate_background(style.handle.background, 0.1);
}
}
style
});
}
None => {}
}
}
if !circular && sp.style.is_none() && has_rail_overrides {
s = s.style(move |theme: &iced::Theme, status| {
let mut style = slider::default(theme, status);
apply_rail_overrides(&mut style, rail_color, rail_width);
style
});
}
{
let status_wid = ctx.window_id.to_string();
let status_id = node.id.clone();
s = s.on_status_change(move |status| Message::Event {
window_id: status_wid.clone(),
id: status_id.clone(),
value: Value::String(status.to_string()),
family: "status".into(),
});
}
container(s).id(widget::Id::from(node.id.clone())).into()
}
struct VerticalSliderProps {
value: Option<f64>,
step: Option<f64>,
keyboard_step: Option<f64>,
height: Option<Length>,
default: Option<f64>,
shift_step: Option<f64>,
label: Option<String>,
rail_color: Option<Color>,
style: Option<CoreStyle>,
}
impl VerticalSliderProps {
fn from_node(node: &TreeNode) -> Self {
let p = &node.props;
Self {
value: f64::extract(p, "value"),
step: f64::extract(p, "step"),
keyboard_step: f64::extract(p, "keyboard_step"),
height: Length::extract(p, "height"),
default: f64::extract(p, "default"),
shift_step: f64::extract(p, "shift_step"),
label: String::extract(p, "label"),
rail_color: Color::extract(p, "rail_color"),
style: CoreStyle::extract(p, "style"),
}
}
}
fn render_vertical_slider<'a, R: PlushieRenderer>(
node: &'a TreeNode,
ctx: RenderCtx<'a, R>,
) -> Element<'a, Message, Theme, R> {
let props = &node.props;
let vp = VerticalSliderProps::from_node(node);
let range = prop_range_f64(props);
let value = vp.value.unwrap_or(*range.start());
let width = prop_animated_f32(&ctx.caches.interpolated_props, &node.id, props, "width");
let height = vp
.height
.as_ref()
.map(iced_convert::length)
.unwrap_or(iced::Length::Fill);
let id = node.id.clone();
let release_id = node.id.clone();
let window_id = ctx.window_id.to_string();
let release_window_id = window_id.clone();
let mut s = vertical_slider(range, value, move |v| Message::Event {
window_id: window_id.clone(),
id: id.clone(),
value: serde_json::json!(v),
family: "slide".into(),
})
.on_release(Message::Event {
window_id: release_window_id,
id: release_id,
value: Value::Null,
family: "slide_release".into(),
})
.height(height);
if let Some(w) = width {
s = s.width(w);
}
if let Some(st) = effective_slider_step(vp.step, vp.keyboard_step) {
s = s.step(st);
}
if let Some(d) = vp.default {
s = s.default(d);
}
if let Some(ss) = vp.shift_step {
s = s.shift_step(ss);
}
if let Some(label) = vp.label {
s = s.label(label);
}
let rail_color = vp.rail_color.as_ref().map(iced_convert::color);
let rail_width = prop_f32(props, "rail_width");
let has_rail_overrides = rail_color.is_some() || rail_width.is_some();
let circular = prop_bool_default(props, "circular_handle", false);
if circular {
let radius = prop_f32(props, "handle_radius").unwrap_or(8.0);
s = s.style(move |theme, status| {
let mut style = vertical_slider::default(theme, status).with_circular_handle(radius);
apply_rail_overrides(&mut style, rail_color, rail_width);
style
});
} else {
match &vp.style {
Some(CoreStyle::Preset(name)) => {
s = match name.as_str() {
"default" => {
if has_rail_overrides {
s.style(move |theme: &iced::Theme, status| {
let mut style = vertical_slider::default(theme, status);
apply_rail_overrides(&mut style, rail_color, rail_width);
style
})
} else {
s.style(vertical_slider::default)
}
}
_ => {
log::warn!(
"unknown style {:?} for widget type {:?}, using default",
name,
"vertical_slider"
);
s
}
};
}
Some(CoreStyle::Custom(style_map)) => {
let ov = style_overrides_from_style_map(&node.id, style_map, ctx.caches);
s = s.style(move |theme: &iced::Theme, status| {
let mut style = vertical_slider::default(theme, status);
apply_slider_handle_fields(&mut style.handle, &ov.base);
apply_rail_overrides(&mut style, rail_color, rail_width);
if matches!(status, vertical_slider::Status::Hovered) {
if let Some(ref f) = ov.hovered {
apply_slider_handle_fields(&mut style.handle, f);
} else {
style.handle.background =
deviate_background(style.handle.background, 0.1);
}
}
style
});
}
None => {}
}
if vp.style.is_none() && has_rail_overrides {
s = s.style(move |theme: &iced::Theme, status| {
let mut style = vertical_slider::default(theme, status);
apply_rail_overrides(&mut style, rail_color, rail_width);
style
});
}
}
{
let status_wid = ctx.window_id.to_string();
let status_id = node.id.clone();
s = s.on_status_change(move |status| Message::Event {
window_id: status_wid.clone(),
id: status_id.clone(),
value: Value::String(status.to_string()),
family: "status".into(),
});
}
container(s).id(widget::Id::from(node.id.clone())).into()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn keyboard_step_overrides_base_step() {
assert_eq!(effective_slider_step(Some(1.0), Some(5.0)), Some(5.0));
}
#[test]
fn keyboard_step_clamps_to_positive_step() {
assert_eq!(
effective_slider_step(Some(1.0), Some(0.0)),
Some(f64::EPSILON)
);
assert_eq!(
effective_slider_step(Some(1.0), Some(-1.0)),
Some(f64::EPSILON)
);
}
#[test]
fn base_step_is_used_without_keyboard_step() {
assert_eq!(effective_slider_step(Some(2.0), None), Some(2.0));
}
fn infer_slider(props: serde_json::Value) -> Option<A11yOverrides> {
let node = crate::testing::node_with_props("s", "slider", props);
let widget = SliderWidget::new();
<SliderWidget as PlushieWidget<iced::Renderer>>::infer_a11y(&widget, &node)
}
fn infer_vertical(props: serde_json::Value) -> Option<A11yOverrides> {
let node = crate::testing::node_with_props("vs", "vertical_slider", props);
let widget = VerticalSliderWidget::new();
<VerticalSliderWidget as PlushieWidget<iced::Renderer>>::infer_a11y(&widget, &node)
}
#[test]
fn slider_mnemonic_propagates() {
let o = infer_slider(json!({"mnemonic": "V"})).expect("mnemonic should infer");
assert_eq!(o.core().mnemonic, Some('V'));
}
#[test]
fn slider_no_mnemonic_returns_none() {
assert!(infer_slider(json!({})).is_none());
}
#[test]
fn vertical_slider_mnemonic_propagates() {
let o = infer_vertical(json!({"mnemonic": "V"})).expect("mnemonic should infer");
assert_eq!(o.core().mnemonic, Some('V'));
}
}