use floem_reactive::{create_effect, SignalGet, SignalUpdate};
use floem_winit::keyboard::{Key, NamedKey};
use peniko::kurbo::{Circle, Point, RoundedRect};
use peniko::{Brush, Color};
use crate::unit::Pct;
use crate::{
event::EventPropagation,
id::ViewId,
prop, prop_extractor,
style::{Background, BorderRadius, CustomStylable, Foreground, Height, Style},
style_class,
unit::{PxPct, PxPctAuto},
view::View,
views::Decorators,
Renderer,
};
pub fn slider<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Slider {
Slider::new(percent)
}
enum SliderUpdate {
Percent(f64),
}
prop!(pub EdgeAlign: bool {} = false);
prop!(pub HandleRadius: PxPct {} = PxPct::Pct(98.));
prop_extractor! {
SliderStyle {
foreground: Foreground,
handle_radius: HandleRadius,
edge_align: EdgeAlign,
}
}
style_class!(pub SliderClass);
style_class!(pub BarClass);
style_class!(pub AccentBarClass);
prop_extractor! {
BarStyle {
border_radius: BorderRadius,
color: Background,
height: Height
}
}
pub struct Slider {
id: ViewId,
onchangepx: Option<Box<dyn Fn(f64)>>,
onchangepct: Option<Box<dyn Fn(Pct)>>,
held: bool,
percent: f64,
prev_percent: f64,
base_bar_style: BarStyle,
accent_bar_style: BarStyle,
handle: Circle,
base_bar: RoundedRect,
accent_bar: RoundedRect,
size: taffy::prelude::Size<f32>,
style: SliderStyle,
}
impl View for Slider {
fn id(&self) -> ViewId {
self.id
}
fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
if let Ok(update) = state.downcast::<SliderUpdate>() {
match *update {
SliderUpdate::Percent(percent) => self.percent = percent,
}
self.id.request_layout();
}
}
fn event_before_children(
&mut self,
cx: &mut crate::context::EventCx,
event: &crate::event::Event,
) -> EventPropagation {
let pos_changed = match event {
crate::event::Event::PointerDown(event) => {
cx.update_active(self.id());
self.id.request_layout();
self.held = true;
self.percent = event.pos.x / self.size.width as f64 * 100.;
true
}
crate::event::Event::PointerUp(event) => {
self.id.request_layout();
let changed = self.held;
if self.held {
self.percent = event.pos.x / self.size.width as f64 * 100.;
self.update_restrict_position();
}
self.held = false;
changed
}
crate::event::Event::PointerMove(event) => {
self.id.request_layout();
if self.held {
self.percent = event.pos.x / self.size.width as f64 * 100.;
true
} else {
false
}
}
crate::event::Event::FocusLost => {
self.held = false;
false
}
crate::event::Event::KeyDown(event) => {
if event.key.logical_key == Key::Named(NamedKey::ArrowLeft) {
self.id.request_layout();
self.percent -= 10.;
true
} else if event.key.logical_key == Key::Named(NamedKey::ArrowRight) {
self.id.request_layout();
self.percent += 10.;
true
} else {
false
}
}
_ => false,
};
self.update_restrict_position();
if pos_changed && self.percent != self.prev_percent {
if let Some(onchangepx) = &self.onchangepx {
onchangepx(self.handle_center());
}
if let Some(onchangepct) = &self.onchangepct {
onchangepct(Pct(self.percent))
}
}
EventPropagation::Continue
}
fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
let style = cx.style();
let mut paint = false;
let base_bar_style = style.clone().apply_class(BarClass);
paint |= self.base_bar_style.read_style(cx, &base_bar_style);
let accent_bar_style = style.apply_class(AccentBarClass);
paint |= self.accent_bar_style.read_style(cx, &accent_bar_style);
paint |= self.style.read(cx);
if paint {
cx.app_state_mut().request_paint(self.id);
}
}
fn compute_layout(
&mut self,
_cx: &mut crate::context::ComputeLayoutCx,
) -> Option<peniko::kurbo::Rect> {
self.update_restrict_position();
let layout = self.id.get_layout().unwrap_or_default();
self.size = layout.size;
let circle_radius = match self.style.handle_radius() {
PxPct::Px(px) => px,
PxPct::Pct(pct) => self.size.width.min(self.size.height) as f64 / 2. * (pct / 100.),
};
let width = self.size.width as f64 - circle_radius * 2.;
let center = width * (self.percent / 100.) + circle_radius;
let circle_point = Point::new(center, (self.size.height / 2.) as f64);
self.handle = crate::kurbo::Circle::new(circle_point, circle_radius);
let base_bar_height = match self.base_bar_style.height() {
PxPctAuto::Px(px) => px,
PxPctAuto::Pct(pct) => self.size.height as f64 * (pct / 100.),
PxPctAuto::Auto => self.size.height as f64,
};
let accent_bar_height = match self.accent_bar_style.height() {
PxPctAuto::Px(px) => px,
PxPctAuto::Pct(pct) => self.size.height as f64 * (pct / 100.),
PxPctAuto::Auto => self.size.height as f64,
};
let base_bar_radius = match self.base_bar_style.border_radius() {
PxPct::Px(px) => px,
PxPct::Pct(pct) => base_bar_height / 2. * (pct / 100.),
};
let accent_bar_radius = match self.accent_bar_style.border_radius() {
PxPct::Px(px) => px,
PxPct::Pct(pct) => accent_bar_height / 2. * (pct / 100.),
};
let mut base_bar_length = self.size.width as f64;
if !self.style.edge_align() {
base_bar_length -= self.handle.radius * 2.;
}
let base_bar_y_start = self.size.height as f64 / 2. - base_bar_height / 2.;
let accent_bar_y_start = self.size.height as f64 / 2. - accent_bar_height / 2.;
let bar_x_start = if self.style.edge_align() {
0.
} else {
self.handle.radius
};
self.base_bar = peniko::kurbo::Rect::new(
bar_x_start,
base_bar_y_start,
bar_x_start + base_bar_length,
base_bar_y_start + base_bar_height,
)
.to_rounded_rect(base_bar_radius);
self.accent_bar = peniko::kurbo::Rect::new(
bar_x_start,
accent_bar_y_start,
self.handle_center(),
accent_bar_y_start + accent_bar_height,
)
.to_rounded_rect(accent_bar_radius);
self.prev_percent = self.percent;
None
}
fn paint(&mut self, cx: &mut crate::context::PaintCx) {
cx.fill(
&self.base_bar,
&self.base_bar_style.color().unwrap_or(Color::BLACK.into()),
0.,
);
cx.save();
cx.clip(&self.base_bar);
cx.fill(
&self.accent_bar,
&self
.accent_bar_style
.color()
.unwrap_or(Color::TRANSPARENT.into()),
0.,
);
cx.restore();
if let Some(color) = self.style.foreground() {
cx.fill(&self.handle, &color, 0.);
}
}
}
impl Slider {
pub fn new<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Self {
let id = ViewId::new();
create_effect(move |_| {
let percent = percent().into();
id.update_state(SliderUpdate::Percent(percent.0));
});
Slider {
id,
onchangepx: None,
onchangepct: None,
held: false,
percent: 0.0,
prev_percent: 0.0,
handle: Default::default(),
base_bar_style: Default::default(),
accent_bar_style: Default::default(),
base_bar: Default::default(),
accent_bar: Default::default(),
size: Default::default(),
style: Default::default(),
}
.class(SliderClass)
.keyboard_navigable()
}
pub fn new_rw(percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static) -> Self {
Self::new(move || percent.get()).on_change_pct(move |pct| percent.set(pct))
}
fn update_restrict_position(&mut self) {
self.percent = self.percent.clamp(0., 100.);
}
fn handle_center(&self) -> f64 {
let width = self.size.width as f64 - self.handle.radius * 2.;
width * (self.percent / 100.) + self.handle.radius
}
pub fn on_change_pct(mut self, onchangepct: impl Fn(Pct) + 'static) -> Self {
self.onchangepct = Some(Box::new(onchangepct));
self
}
pub fn on_change_px(mut self, onchangepx: impl Fn(f64) + 'static) -> Self {
self.onchangepx = Some(Box::new(onchangepx));
self
}
pub fn slider_style(
self,
style: impl Fn(SliderCustomStyle) -> SliderCustomStyle + 'static,
) -> Self {
self.custom_style(style)
}
}
#[derive(Debug, Default, Clone)]
pub struct SliderCustomStyle(Style);
impl From<SliderCustomStyle> for Style {
fn from(val: SliderCustomStyle) -> Self {
val.0
}
}
impl CustomStylable<SliderCustomStyle> for Slider {
type DV = Self;
}
impl SliderCustomStyle {
pub fn new() -> Self {
Self::default()
}
pub fn handle_color(mut self, color: impl Into<Option<Brush>>) -> Self {
self = SliderCustomStyle(self.0.set(Foreground, color));
self
}
pub fn edge_align(mut self, align: bool) -> Self {
self = SliderCustomStyle(self.0.set(EdgeAlign, align));
self
}
pub fn handle_radius(mut self, radius: impl Into<PxPct>) -> Self {
self = SliderCustomStyle(self.0.set(HandleRadius, radius));
self
}
pub fn bar_color(mut self, color: impl Into<Brush>) -> Self {
self = SliderCustomStyle(self.0.class(BarClass, |s| s.background(color)));
self
}
pub fn bar_radius(mut self, radius: impl Into<PxPct>) -> Self {
self = SliderCustomStyle(self.0.class(BarClass, |s| s.border_radius(radius)));
self
}
pub fn bar_height(mut self, height: impl Into<PxPctAuto>) -> Self {
self = SliderCustomStyle(self.0.class(BarClass, |s| s.height(height)));
self
}
pub fn accent_bar_color(mut self, color: impl Into<Brush>) -> Self {
self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.background(color)));
self
}
pub fn accent_bar_radius(mut self, radius: impl Into<PxPct>) -> Self {
self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.border_radius(radius)));
self
}
pub fn accent_bar_height(mut self, height: impl Into<PxPctAuto>) -> Self {
self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.height(height)));
self
}
}