Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix slider and add example #279

Merged
merged 1 commit into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/widget-gallery/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod labels;
pub mod lists;
pub mod radio_buttons;
pub mod rich_text;
pub mod slider;

use floem::{
event::{Event, EventListener},
Expand Down Expand Up @@ -38,6 +39,7 @@ fn app_view() -> impl View {
"RichText",
"Image",
"Clipboard",
"Slider",
]
.into_iter()
.collect();
Expand Down Expand Up @@ -159,6 +161,7 @@ fn app_view() -> impl View {
"RichText" => container_box(rich_text::rich_text_view()),
"Image" => container_box(images::img_view()),
"Clipboard" => container_box(clipboard::clipboard_view()),
"Slider" => container_box(slider::slider_view()),
_ => container_box(label(|| "Not implemented".to_owned())),
},
)
Expand Down
62 changes: 59 additions & 3 deletions examples/widget-gallery/src/slider.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,59 @@
// slider::slider(|| 0.)
// .style(|s| s.height(15).width(200))
// .on_change_pct(move |val| set_slider.set(val)),
use floem::{
peniko::Color,
reactive::{create_effect, create_rw_signal},
style::Foreground,
unit::UnitExt,
view::View,
views::{label, stack, text_input, Decorators},
widgets::slider,
};

use crate::form::{self, form_item};

pub fn slider_view() -> impl View {
let set_slider = create_rw_signal(50.);
let input = create_rw_signal(String::from("50"));
create_effect(move |_| {
set_slider.set(input.get().parse::<f32>().unwrap_or_default());
});
form::form({
(
form_item("Input Control:".to_string(), 120.0, move || {
text_input(input)
}),
form_item("Default Slider:".to_string(), 120.0, move || {
stack((
slider::slider(move || set_slider.get())
.style(|s| s.width(200))
.on_change_pct(move |val| set_slider.set(val)),
label(move || format!("{:.1}%", set_slider.get())),
))
.style(|s| s.gap(10., 10))
}),
form_item("Unaligned Slider:".to_string(), 120.0, move || {
stack((
slider::slider(move || set_slider.get())
.style(|s| {
s.width(200)
.class(slider::AccentBarClass, |s| s.height(30.pct()))
.class(slider::BarClass, |s| s.height(30.pct()))
.set(slider::EdgeAlign, false)
})
.on_change_pct(move |val| set_slider.set(val)),
label(move || format!("{:.1}%", set_slider.get())),
))
.style(|s| s.gap(10., 10))
}),
form_item("Progress bar:".to_string(), 120.0, move || {
stack((
slider::slider(move || set_slider.get())
.style(|s| s.width(200).set(Foreground, Color::GREEN))
.disable_events(|| true)
.on_change_pct(move |val| set_slider.set(val)),
label(move || format!("{:.1}%", set_slider.get())),
))
.style(|s| s.gap(10., 10))
}),
)
})
}
5 changes: 3 additions & 2 deletions src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,10 @@ pub(crate) fn default_theme() -> Theme {
})
.class(SliderClass, |s| {
s.set(Foreground, Color::DARK_GRAY)
.set(slider::BarExtends, true)
.height(15)
.width(100)
.set(slider::EdgeAlign, true)
.set(slider::HandleRadius, PxPct::Pct(100.))
.set(slider::BarExtends, false)
})
.class(PlaceholderTextClass, |s| {
s.color(Color::rgba8(158, 158, 158, 30))
Expand Down
91 changes: 44 additions & 47 deletions src/widgets/slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ enum SliderUpdate {
Percent(f32),
}

prop!(pub BarExtends: bool {} = false);
prop!(pub EdgeAlign: bool {} = false);
prop!(pub HandleRadius: PxPct {} = PxPct::Pct(98.));

prop_extracter! {
SliderStyle {
foreground: Foreground,
handle_radius: HandleRadius,
bar_extends: BarExtends,
edge_align: EdgeAlign,
}
}
style_class!(pub SliderClass);
Expand All @@ -50,8 +50,8 @@ pub struct Slider {
onchangepx: Option<Box<dyn Fn(f32)>>,
onchangepct: Option<Box<dyn Fn(f32)>>,
held: bool,
position: f32,
prev_position: f32,
percent: f32,
prev_percent: f32,
base_bar_style: BarStyle,
accent_bar_style: BarStyle,
handle: Circle,
Expand All @@ -66,18 +66,19 @@ pub struct Slider {
///
/// You can set the slider to a percent value betwen 0 and 100.
///
/// The slider is composed of three parts. The main background bar, an accent bar and a handle.
/// The slider is composed of four parts. The main view, the background bar, an accent bar and a handle.
/// The background bar is separate from the main view because it is shortened when [`EdgeAlign`] is set to false;
///
/// **Responding to events**:
/// You can respond to events by calling the [`Slider::on_change_pct`], and [`Slider::on_change_px`] methods on [`Slider`] and passing in a callback. Both of these callbacks are called whenever a change is effected by either clicking or by the arrow keys.
/// These callbacks will not be called on reactive updates, only on a mouse event or by using the arrrow keys.
///
/// You can also disable event hanlidng of mouse clicks and arrow keys using [`Slider::disable_events`]. If you want to use this slider as a progress bar this may be useful.
/// You can also disable event handlidng of mouse clicks and arrow keys using [`Slider::disable_events`]. If you want to use this slider as a progress bar this may be useful.
///
/// **Styling**:
/// You can set three properties on the slider (`SliderClass`): [`Foreground`] color and [`HandleRadius`, which both affect the handle, and [`BarExends`.
/// You can set three properties on the slider (`SliderClass`): [`Foreground`] color and [`HandleRadius`], which both affect the handle, and [`EdgeAlign`].
/// You can set the [`HandleRadius`] to either be a pixel value or a percent value. If you set it to a percent it is relative to the main height of the view. 50% radius will make the handle fill the background.
/// If you set [`BarExtends`] to [`true`, at 0% and 100% the edges of the handle will be within the bar.If you set it to false then the bar will go half way through the handle.
/// If you set [`EdgeAlign`] to `true`, at 0% and 100% the edges of the handle will be within the bar.If you set it to `false` then the bars will be shortened and the handle will appear to have it's center at the ends of the bar.
///
/// You can set properties on the bars as well. The bar (`BarClass`) and accent bar (`AccentBarClass`) both have a [`BorderRadius`] and [`Background`] color. You can also set a height on the accent bar.
// The height of the main bar will bet set to the height of the main view.
Expand All @@ -94,7 +95,7 @@ pub struct Slider {
/// .style(|s|
/// s.class(slider::SliderClass, |s| {
/// s.set(Foreground, Color::WHITE)
/// .set(slider::BarExtends, true)
/// .set(slider::EdgeAlign, true)
/// .set(slider::HandleRadius, 50.pct())
/// })
/// .class(slider::BarClass, |s| {
Expand All @@ -119,8 +120,8 @@ pub fn slider(percent: impl Fn() -> f32 + 'static) -> Slider {
onchangepx: None,
onchangepct: None,
held: false,
position: 0.0,
prev_position: 0.0,
percent: 0.0,
prev_percent: 0.0,
handle: Default::default(),
base_bar_style: Default::default(),
accent_bar_style: Default::default(),
Expand All @@ -147,9 +148,7 @@ impl View for Slider {
if let Ok(update) = state.downcast::<SliderUpdate>() {
match *update {
SliderUpdate::DisableEvents(disable) => self.disable_events = disable,
SliderUpdate::Percent(percent) => {
self.position = self.size.width * (percent / 100.)
}
SliderUpdate::Percent(percent) => self.percent = percent,
}
cx.request_layout(self.id());
}
Expand All @@ -167,7 +166,7 @@ impl View for Slider {
cx.update_active(self.id());
cx.app_state_mut().request_layout(self.id());
self.held = true;
self.position = event.pos.x as f32;
self.percent = event.pos.x as f32 / self.size.width * 100.;
true
}
crate::event::Event::PointerUp(event) => {
Expand All @@ -176,7 +175,7 @@ impl View for Slider {
// set the state based on the position of the slider
let changed = self.held;
if self.held {
self.position = event.pos.x as f32;
self.percent = event.pos.x as f32 / self.size.width * 100.;
self.update_restrict_position();
}
self.held = false;
Expand All @@ -185,7 +184,7 @@ impl View for Slider {
crate::event::Event::PointerMove(event) => {
cx.app_state_mut().request_layout(self.id());
if self.held {
self.position = event.pos.x as f32;
self.percent = event.pos.x as f32 / self.size.width * 100.;
true
} else {
false
Expand All @@ -198,11 +197,11 @@ impl View for Slider {
crate::event::Event::KeyDown(event) => {
if event.key.logical_key == Key::Named(NamedKey::ArrowLeft) {
cx.app_state_mut().request_layout(self.id());
self.position -= (self.size.width - self.handle.radius as f32 * 2.) * 0.1;
self.percent -= 10.;
true
} else if event.key.logical_key == Key::Named(NamedKey::ArrowRight) {
cx.app_state_mut().request_layout(self.id());
self.position += (self.size.width - self.handle.radius as f32 * 2.) * 0.1;
self.percent += 10.;
true
} else {
false
Expand All @@ -213,20 +212,12 @@ impl View for Slider {

self.update_restrict_position();

if pos_changed && self.position != self.prev_position {
let circle_radius = match self.style.handle_radius() {
PxPct::Px(px) => px as f32,
PxPct::Pct(pct) => {
self.size.width.min(self.size.height) / 2. * (pct as f32 / 100.)
}
};
if pos_changed && self.percent != self.prev_percent {
if let Some(onchangepx) = &self.onchangepx {
onchangepx(self.position);
onchangepx(self.handle_center());
}
if let Some(onchangepct) = &self.onchangepct {
onchangepct(
(self.position - circle_radius) / (self.size.width - circle_radius * 2.),
)
onchangepct(self.percent)
}
}
}
Expand Down Expand Up @@ -258,34 +249,38 @@ impl View for Slider {
PxPct::Px(px) => px as f32,
PxPct::Pct(pct) => self.size.width.min(self.size.height) / 2. * (pct as f32 / 100.),
};
let circle_point = Point::new(self.position as f64, (self.size.height / 2.) as f64);
let circle_point = Point::new(self.handle_center() as f64, (self.size.height / 2.) as f64);
self.handle = crate::kurbo::Circle::new(circle_point, circle_radius as f64);

let base_bar_thickness = self.size.height as f64;
let accent_bar_thickness = match self.accent_bar_style.height() {
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_thickness / 2. * (pct / 100.),
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_thickness / 2. * (pct / 100.),
PxPct::Pct(pct) => accent_bar_height / 2. * (pct / 100.),
};

let mut base_bar_length = self.size.width as f64;
if !self.style.bar_extends() {
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_thickness / 2.;
let accent_bar_y_start = self.size.height as f64 / 2. - accent_bar_thickness / 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.bar_extends() {
let bar_x_start = if self.style.edge_align() {
0.
} else {
self.handle.radius
Expand All @@ -295,18 +290,18 @@ impl View for Slider {
bar_x_start,
base_bar_y_start,
bar_x_start + base_bar_length,
base_bar_y_start + base_bar_thickness,
base_bar_y_start + base_bar_height,
)
.to_rounded_rect(base_bar_radius);
self.accent_bar = kurbo::Rect::new(
bar_x_start,
accent_bar_y_start,
self.position as f64,
accent_bar_y_start + accent_bar_thickness,
self.handle_center() as f64,
accent_bar_y_start + accent_bar_height,
)
.to_rounded_rect(accent_bar_radius);

self.prev_position = self.position;
self.prev_percent = self.percent;

None
}
Expand All @@ -332,10 +327,12 @@ impl View for Slider {
}
impl Slider {
fn update_restrict_position(&mut self) {
self.position = self
.position
.max(self.handle.radius as f32)
.min(self.size.width - self.handle.radius as f32);
self.percent = self.percent.clamp(0., 100.);
}

fn handle_center(&self) -> f32 {
let width = self.size.width - self.handle.radius as f32 * 2.;
width * (self.percent / 100.) + self.handle.radius as f32
}

/// Add an event handler to be run when the button is toggled.
Expand Down