Skip to content
This repository has been archived by the owner on May 12, 2022. It is now read-only.

Commit

Permalink
feat(ui): support displaying tick marks in Slider
Browse files Browse the repository at this point in the history
  • Loading branch information
yvt committed May 17, 2020
1 parent 0b990b5 commit bf9a420
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 13 deletions.
1 change: 1 addition & 0 deletions tcw3/examples/tcw3_widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ fn main() {

let slider = Slider::new(style_manager, false);
let slider = Rc::new(slider);
slider.set_uniform_ticks(8);
{
let slider_weak = Rc::downgrade(&slider);
slider.set_on_drag(move |_| {
Expand Down
1 change: 1 addition & 0 deletions tcw3/src/ui/theming/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub mod elem_id {
pub const SPLITTER: ClassSet = ClassSet::id(iota + SYS_START_VALUE);
, TEXT_SELECTION
, SLIDER_KNOB
, SLIDER_TICKS
}
}

Expand Down
29 changes: 27 additions & 2 deletions tcw3/src/ui/theming/stylesheet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,12 +548,14 @@ const SCROLLBAR_VISUAL_RADIUS: f32 = SCROLLBAR_VISUAL_WIDTH / 2.0;
const SCROLLBAR_MARGIN: f32 = 6.0;
const SCROLLBAR_LEN_MIN: f32 = 20.0;

const SLIDER_WIDTH: f32 = 18.0;
const SLIDER_WIDTH: f32 = 28.0;
const SLIDER_TROUGH_WIDTH: f32 = 1.0;
const SLIDER_KNOB_SIZE: f32 = 16.0;
const SLIDER_KNOB_RADIUS: f32 = SLIDER_KNOB_SIZE / 2.0;
const SLIDER_LEN_MARGIN: f32 = 10.0;
const SLIDER_LEN_MIN: f32 = SLIDER_LEN_MARGIN * 2.0 + 10.0;
const SLIDER_TICKS_DISTANCE: f32 = 4.0;
const SLIDER_TICKS_SIZE: f32 = 3.0;

const FIELD_HEIGHT: f32 = 20.0;

Expand Down Expand Up @@ -862,6 +864,15 @@ lazy_static! {
margin: [NAN, SLIDER_LEN_MARGIN - SLIDER_KNOB_RADIUS, NAN, SLIDER_LEN_MARGIN - SLIDER_KNOB_RADIUS],
.. Metrics::default()
},
subview_metrics[roles::SLIDER_TICKS]: Metrics {
margin: [
SLIDER_WIDTH * 0.5 + SLIDER_KNOB_RADIUS +SLIDER_TICKS_DISTANCE,
SLIDER_LEN_MARGIN,
NAN,
SLIDER_LEN_MARGIN,
],
size: Vector2::new(NAN, SLIDER_TICKS_SIZE),
},
allow_grow: [true, false],
min_size: Vector2::new(SLIDER_LEN_MIN, SLIDER_WIDTH),

Expand All @@ -875,6 +886,15 @@ lazy_static! {
margin: [SLIDER_LEN_MARGIN - SLIDER_KNOB_RADIUS, NAN, SLIDER_LEN_MARGIN - SLIDER_KNOB_RADIUS, NAN],
.. Metrics::default()
},
subview_metrics[roles::SLIDER_TICKS]: Metrics {
margin: [
SLIDER_LEN_MARGIN,
NAN,
SLIDER_LEN_MARGIN,
SLIDER_WIDTH * 0.5 + SLIDER_KNOB_RADIUS +SLIDER_TICKS_DISTANCE,
],
size: Vector2::new(NAN, SLIDER_TICKS_SIZE),
},
allow_grow: [false, true],
min_size: Vector2::new(SLIDER_WIDTH, SLIDER_LEN_MIN),

Expand All @@ -884,7 +904,7 @@ lazy_static! {
},
},

// Slider thumb
// Slider knob
([] < [.SLIDER]) (priority = 100) {
num_layers: 1,
#[dyn] layer_img[0]: Some(recolor_tint(&assets::SLIDER_KNOB)),
Expand All @@ -899,6 +919,11 @@ lazy_static! {
#[dyn] layer_img[0]: Some(recolor_tint(&assets::SLIDER_KNOB_ACT)),
},

// Slider tick marks
([#SLIDER_TICKS]) (priority = 100) {
fg_color: RGBAF32::new(0.5, 0.5, 0.5, 1.0),
},

// Splitter
([#SPLITTER]) (priority = 100) {
num_layers: 1,
Expand Down
176 changes: 165 additions & 11 deletions tcw3/src/ui/views/slider.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Implements the slider.
use alt_fp::FloatOrd;
use cggeom::prelude::*;
use cggeom::{box2, prelude::*};
use cgmath::Point2;
use std::{
cell::{Cell, RefCell},
Expand All @@ -10,14 +10,18 @@ use std::{

use crate::{
pal,
prelude::*,
ui::{
layouts::FillLayout,
mixins::CanvasMixin,
theming,
theming::{
elem_id, roles, ClassSet, HElem, Manager, ModifyArrangementArgs, PropKindFlags,
StyledBox, StyledBoxOverride, Widget,
elem_id, roles, ClassSet, GetPropValue, HElem, Manager, ModifyArrangementArgs,
PropKindFlags, StyledBox, StyledBoxOverride, Widget,
},
},
uicore::{HView, HViewRef, MouseDragListener, ViewFlags, ViewListener},
uicore::{HView, HViewRef, HWndRef, MouseDragListener, UpdateCtx, ViewFlags, ViewListener},
utils::resetiter,
};

// Reuse some items from the scrollbar implementation
Expand Down Expand Up @@ -46,18 +50,21 @@ pub use super::scrollbar::{Dir, ScrollbarDragListener};
/// would. You need to make sure the maximum size is set to infinity to
/// achieve a desired effect.
///
/// - [`subviews[roles::SLIDER_TICKS]`]: The container for ticks. Should
/// align with the movable range of the knob for it to make sense to the
/// application user.
/// - [`subviews[roles::SLIDER_TICKS]`]: The container for tick marks.
/// Should align with the movable range of the knob for it to make sense
/// to the application user.
///
/// - `style_elem > *` - Custom label views.
///
/// - `style_elem > `[`#SLIDER_KNOB`] - The knob. See
/// [`StyledBox`](crate::ui::theming::StyledBox)
///
/// - `style_elem > `[`#SLIDER_TICKS`] - The ticks. Supports `FgColor`.
///
/// [`subviews[roles::SLIDER_KNOB]`]: crate::ui::theming::roles::SLIDER_KNOB
/// [`subviews[roles::SLIDER_TICKS]`]: crate::ui::theming::roles::SLIDER_TICKS
/// [`#SLIDER_KNOB`]: crate::ui::theming::elem_id::SLIDER_KNOB
/// [`#SLIDER_TICKS`]: crate::ui::theming::elem_id::SLIDER_TICKS
///
#[derive(Debug)]
pub struct Slider {
Expand All @@ -73,6 +80,11 @@ struct Shared {
frame: StyledBox,
knob: StyledBox,
layout_state: Cell<LayoutState>,

ticks_view: HView,
ticks_elem: theming::Elem,
ticks_state: Rc<RefCell<TicksState>>,
ticks_empty: Cell<bool>,
}

type DragHandler = Box<dyn Fn(pal::Wm) -> Box<dyn ScrollbarDragListener>>;
Expand Down Expand Up @@ -113,13 +125,24 @@ impl Slider {
});
frame.set_auto_class_set(ClassSet::HOVER | ClassSet::FOCUS);

let wrapper = HView::new(ViewFlags::ACCEPT_MOUSE_DRAG);
wrapper.set_layout(FillLayout::new(frame.view()));

// Tick marks
let ticks_view = HView::new(ViewFlags::default());
let ticks_elem = theming::Elem::new(style_manager);
ticks_elem.set_class_set(elem_id::SLIDER_TICKS);
let ticks_state = Rc::new(RefCell::new(TicksState {
canvas: CanvasMixin::new(),
values: Box::new(resetiter::empty()),
}));
frame.set_subelement(roles::SLIDER_TICKS, Some(ticks_elem.helem()));

// Knob
let knob = StyledBox::new(style_manager, ViewFlags::default());
knob.set_class_set(elem_id::SLIDER_KNOB);
frame.set_child(roles::SLIDER_KNOB, Some(&knob));

let wrapper = HView::new(ViewFlags::ACCEPT_MOUSE_DRAG);
wrapper.set_layout(FillLayout::new(frame.view()));

let shared = Rc::new(Shared {
vertical,
value: Cell::new(0.0),
Expand All @@ -129,6 +152,10 @@ impl Slider {
frame,
knob,
layout_state: Cell::new(LayoutState::default()),
ticks_view,
ticks_elem,
ticks_state,
ticks_empty: Cell::new(true),
});

Shared::update_sb_override(&shared);
Expand All @@ -137,6 +164,11 @@ impl Slider {
shared: Rc::downgrade(&shared),
});

shared.ticks_view.set_listener(TicksViewListener {
shared: Rc::downgrade(&shared),
state: Rc::clone(&shared.ticks_state),
});

Self { shared }
}

Expand Down Expand Up @@ -177,6 +209,53 @@ impl Slider {
Shared::update_sb_override(&self.shared);
}

/// Set the tick mark positions.
pub fn set_ticks<I>(&self, new_ticks: I)
where
I: resetiter::IntoResetIter<Item = f64>,
I::IntoResetIter: 'static,
{
self.set_ticks_inner(Box::new(new_ticks.into_reset_iter()));
}

fn set_ticks_inner(&self, mut new_ticks: Box<dyn resetiter::ResetIter<Item = f64>>) {
let is_empty = new_ticks.is_empty();

let mut ticks_state = self.shared.ticks_state.borrow_mut();
ticks_state.values = new_ticks;

if is_empty != self.shared.ticks_empty.get() {
self.shared.ticks_empty.set(is_empty);
self.shared.frame.set_subview(
roles::SLIDER_TICKS,
if is_empty {
None
} else {
Some(self.shared.ticks_view.clone())
},
);
}

if !is_empty {
ticks_state
.canvas
.pend_draw(self.shared.ticks_view.as_ref());
}
}

/// Arrange tick marks uniformly by calling `set_ticks`.
///
/// `num_segments` must not be zero.
pub fn set_uniform_ticks(&self, num_segments: usize) {
debug_assert!(num_segments != 0);
let num_segments_f64 = num_segments as f64;
self.set_ticks(
(0..=num_segments)
.into_reset_iter()
.map(move |i| i as f64 / num_segments_f64),
);
}

/// Set the factory function for gesture event handlers used when the user
/// grabs the knob.
///
Expand Down Expand Up @@ -265,7 +344,15 @@ impl StyledBoxOverride for SlStyledBoxOverride {
return;
};

assert_eq!(role, roles::SLIDER_KNOB, "TODO: support other roles");
match role {
roles::SLIDER_KNOB => {}
roles::SLIDER_TICKS => {
return;
}
_ => {
todo!();
}
}

let pri = shared.vertical as usize;

Expand Down Expand Up @@ -414,6 +501,73 @@ impl MouseDragListener for SlMouseDragListener {
}
}

/// Implements `ViewListener` for the view displaying tick marks.
struct TicksViewListener {
shared: Weak<Shared>,
state: Rc<RefCell<TicksState>>,
}

struct TicksState {
canvas: CanvasMixin,
values: Box<dyn resetiter::ResetIter<Item = f64>>,
}

impl ViewListener for TicksViewListener {
fn mount(&self, wm: pal::Wm, view: HViewRef<'_>, wnd: HWndRef<'_>) {
self.state.borrow_mut().canvas.mount(wm, view, wnd);
}

fn unmount(&self, wm: pal::Wm, view: HViewRef<'_>) {
self.state.borrow_mut().canvas.unmount(wm, view);
}

fn position(&self, wm: pal::Wm, view: HViewRef<'_>) {
self.state.borrow_mut().canvas.position(wm, view);
}

fn update(&self, wm: pal::Wm, view: HViewRef<'_>, ctx: &mut UpdateCtx<'_>) {
let mut state = self.state.borrow_mut();
let state = &mut *state; // enable split borrow

// Make the visual bounds slightly larger than the frame so that
// the tickmarks at `0` and `1` are not clipped
let size = view.frame().size();
let visual_bounds = box2! {
top_left: [-1.0, 0.0],
size: [size.x + 2.0, size.y]
};

// The tick marks' color
let color = if let Some(shared) = self.shared.upgrade() {
shared.ticks_elem.computed_values().fg_color()
} else {
[0.0, 0.0, 0.0, 0.0].into()
};

// Render all tick marks in a single layer
let values = &mut *state.values;
state
.canvas
.update_layer(wm, view, ctx.hwnd(), visual_bounds, move |draw_ctx| {
let c = &mut draw_ctx.canvas;

c.set_stroke_rgb(color);
c.begin_path();

for x in values.iter().map(|x| x as f32 * size.x) {
c.move_to([x, 0.0].into());
c.line_to([x, size.y].into());
}

c.stroke();
});

if ctx.layers().len() != 1 {
ctx.set_layers(vec![state.canvas.layer().unwrap().clone()]);
}
}
}

#[cfg(test)]
mod tests {
use cgmath::assert_abs_diff_eq;
Expand Down

0 comments on commit bf9a420

Please sign in to comment.