use crate::*;
use std::fmt::Write;
use super::textbox::textbox_handle;
#[derive(Clone)]
pub struct Slider {
pub value: Real,
pub low: Real,
pub high: Real,
pub step: Real,
pub precision: usize,
pub font: FontChoice,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
pub edit: NumberEditState,
}
impl Slider {
pub fn new(value: Real, low: Real, high: Real) -> Self {
Self {
value,
low,
high,
step: 0.0,
precision: 0,
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::GRAB_SCROLL,
edit: NumberEditState::default(),
}
}
pub fn with_opt(value: Real, low: Real, high: Real, step: Real, precision: usize, opt: WidgetOption) -> Self {
Self {
value,
low,
high,
step,
precision,
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::GRAB_SCROLL,
edit: NumberEditState::default(),
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let mut label = String::new();
let _ = write!(label, "{:.*}", self.precision, self.value);
let font = style.resolve_font_choice(self.font);
let text_w = atlas.get_text_size(font, label.as_str()).width;
let padding = style.padding.max(0);
let vertical_pad = (padding / 2).max(1);
let font_height = atlas.get_font_height(font) as i32;
let thumb_size = style.thumb_size.max(0);
let width = (text_w + padding * 2 + thumb_size).max(0);
let height = (font_height.max(thumb_size) + vertical_pad * 2).max(0);
Dimensioni::new(width, height)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
let base = ctx.rect();
let last = self.value;
let mut v = last;
let font = ctx.style().resolve_font_choice(self.font);
if !number_textbox_handle(ctx, control, &mut self.edit, self.precision, font, &mut v).is_none() {
return res;
}
if let Some(delta) = control.scroll_delta {
let range = self.high - self.low;
if range != 0.0 {
let wheel = if delta.y != 0 { delta.y.signum() } else { delta.x.signum() };
if wheel != 0 {
let step_amount = if self.step != 0. { self.step } else { range / 100.0 };
v += wheel as Real * step_amount;
if self.step != 0. {
v = (v + self.step / 2 as Real) / self.step * self.step;
}
}
}
}
let input = ctx.input_or_default();
let range = self.high - self.low;
if control.focused && (!input.mouse_down.is_none() || input.mouse_pressed.is_left()) && base.width > 0 && range != 0.0 {
v = self.low + input.mouse_pos.x as Real * range / base.width as Real;
if self.step != 0. {
v = (v + self.step / 2 as Real) / self.step * self.step;
}
}
if range == 0.0 {
v = self.low;
}
v = if self.high < (if self.low > v { self.low } else { v }) {
self.high
} else if self.low > v {
self.low
} else {
v
};
self.value = v;
if last != v {
res |= ResourceState::CHANGE;
}
ctx.draw_widget_frame(control, base, ControlColor::Base, self.opt);
let w = ctx.style().thumb_size;
let available = (base.width - w).max(0);
let x = if range != 0.0 && available > 0 {
((v - self.low) * available as Real / range) as i32
} else {
0
};
let thumb = rect(base.x + x, base.y, w, base.height);
ctx.draw_widget_frame(control, thumb, ControlColor::Button, self.opt);
self.edit.buf.clear();
let _ = write!(self.edit.buf, "{:.*}", self.precision, self.value);
ctx.draw_control_text_with_font(font, self.edit.buf.as_str(), base, ControlColor::Text, self.opt);
res
}
}
fn number_textbox_handle(
ctx: &mut WidgetCtx<'_>,
control: &ControlState,
edit: &mut NumberEditState,
precision: usize,
font: FontId,
value: &mut Real,
) -> ResourceState {
let shift_click = {
let input = ctx.input_or_default();
input.mouse_pressed.is_left() && input.key_mods.is_shift() && control.hovered
};
if shift_click {
edit.editing = true;
edit.buf.clear();
let _ = write!(edit.buf, "{:.*}", precision, value);
edit.cursor = edit.buf.len();
}
if edit.editing {
let res = textbox_handle(ctx, control, &mut edit.buf, &mut edit.cursor, WidgetOption::NONE, font);
if res.is_submitted() || !control.focused {
if let Ok(v) = edit.buf.parse::<f32>() {
*value = v as Real;
}
edit.editing = false;
edit.cursor = 0;
} else {
return ResourceState::ACTIVE;
}
}
ResourceState::NONE
}
impl Widget for Slider {
fn widget_opt(&self) -> &WidgetOption {
&self.opt
}
fn behaviour_opt(&self) -> &WidgetBehaviourOption {
&self.bopt
}
fn measure(&self, style: &Style, atlas: &AtlasHandle, avail: Dimensioni) -> Dimensioni {
self.preferred_size_widget(style, atlas, avail)
}
fn run(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let old_value = self.value;
let old_edit = self.edit.clone();
let mut res = self.handle_widget(ctx, control);
let changed = self.value != old_value || self.edit != old_edit;
if self.edit.editing || changed {
res |= ResourceState::ACTIVE;
}
res
}
fn effective_widget_opt(&self) -> WidgetOption {
if self.edit.editing { self.opt | WidgetOption::HOLD_FOCUS } else { self.opt }
}
fn needs_input_snapshot(&self) -> bool {
true
}
}
#[derive(Clone)]
pub struct Number {
pub value: Real,
pub step: Real,
pub precision: usize,
pub font: FontChoice,
pub opt: WidgetOption,
pub bopt: WidgetBehaviourOption,
pub edit: NumberEditState,
}
#[derive(Clone, Default, PartialEq)]
pub struct NumberEditState {
pub editing: bool,
pub buf: String,
pub cursor: usize,
}
impl Number {
pub fn new(value: Real, step: Real, precision: usize) -> Self {
Self {
value,
step,
precision,
font: FontChoice::default(),
opt: WidgetOption::NONE,
bopt: WidgetBehaviourOption::NONE,
edit: NumberEditState::default(),
}
}
pub fn with_opt(value: Real, step: Real, precision: usize, opt: WidgetOption) -> Self {
Self {
value,
step,
precision,
font: FontChoice::default(),
opt,
bopt: WidgetBehaviourOption::NONE,
edit: NumberEditState::default(),
}
}
fn preferred_size_widget(&self, style: &Style, atlas: &AtlasHandle, _avail: Dimensioni) -> Dimensioni {
let mut label = String::new();
let _ = write!(label, "{:.*}", self.precision, self.value);
let font = style.resolve_font_choice(self.font);
let text_w = atlas.get_text_size(font, label.as_str()).width;
let padding = style.padding.max(0);
let vertical_pad = (padding / 2).max(1);
let font_height = atlas.get_font_height(font) as i32;
let width = (text_w + padding * 2).max(0);
let height = (font_height + vertical_pad * 2).max(0);
Dimensioni::new(width, height)
}
fn handle_widget(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let mut res = ResourceState::NONE;
let base = ctx.rect();
let last = self.value;
let font = ctx.style().resolve_font_choice(self.font);
if !number_textbox_handle(ctx, control, &mut self.edit, self.precision, font, &mut self.value).is_none() {
return res;
}
let input = ctx.input_or_default();
if control.focused && input.mouse_down.is_left() {
self.value += input.mouse_delta.x as Real * self.step;
}
if self.value != last {
res |= ResourceState::CHANGE;
}
ctx.draw_widget_frame(control, base, ControlColor::Base, self.opt);
self.edit.buf.clear();
let _ = write!(self.edit.buf, "{:.*}", self.precision, self.value);
ctx.draw_control_text_with_font(font, self.edit.buf.as_str(), base, ControlColor::Text, self.opt);
res
}
}
impl Widget for Number {
fn widget_opt(&self) -> &WidgetOption {
&self.opt
}
fn behaviour_opt(&self) -> &WidgetBehaviourOption {
&self.bopt
}
fn measure(&self, style: &Style, atlas: &AtlasHandle, avail: Dimensioni) -> Dimensioni {
self.preferred_size_widget(style, atlas, avail)
}
fn run(&mut self, ctx: &mut WidgetCtx<'_>, control: &ControlState) -> ResourceState {
let old_value = self.value;
let old_edit = self.edit.clone();
let mut res = self.handle_widget(ctx, control);
let changed = self.value != old_value || self.edit != old_edit;
if self.edit.editing || changed {
res |= ResourceState::ACTIVE;
}
res
}
fn effective_widget_opt(&self) -> WidgetOption {
if self.edit.editing { self.opt | WidgetOption::HOLD_FOCUS } else { self.opt }
}
fn needs_input_snapshot(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AtlasSource, FontEntry, SourceFormat};
use std::rc::Rc;
const ICON_NAMES: [&str; 6] = ["white", "close", "expand", "collapse", "check", "expand_down"];
fn make_test_atlas() -> AtlasHandle {
let pixels: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
let icons: Vec<(&str, Recti)> = ICON_NAMES.iter().map(|name| (*name, Recti::new(0, 0, 1, 1))).collect();
let entries = vec![
(
'_',
CharEntry {
offset: Vec2i::new(0, 0),
advance: Vec2i::new(8, 0),
rect: Recti::new(0, 0, 1, 1),
},
),
(
'a',
CharEntry {
offset: Vec2i::new(0, 0),
advance: Vec2i::new(8, 0),
rect: Recti::new(0, 0, 1, 1),
},
),
(
'b',
CharEntry {
offset: Vec2i::new(0, 0),
advance: Vec2i::new(8, 0),
rect: Recti::new(0, 0, 1, 1),
},
),
];
let fonts = vec![(
"default",
FontEntry {
line_size: 10,
baseline: 8,
font_size: 10,
entries: &entries,
},
)];
let source = AtlasSource {
width: 1,
height: 1,
pixels: &pixels,
icons: &icons,
fonts: &fonts,
format: SourceFormat::Raw,
slots: &[],
};
AtlasHandle::from(&source)
}
#[test]
fn slider_zero_range_keeps_value() {
let atlas = make_test_atlas();
let style = Style::default();
let mut commands = Vec::new();
let mut triangle_vertices = Vec::new();
let mut clip_stack = Vec::new();
let mut focus = None;
let mut updated_focus = false;
let mut slider = Slider::new(5.0, 5.0, 5.0);
let slider_id = widget_id_of(&slider);
let rect = rect(0, 0, 100, 20);
let text_input = String::new();
let input = Rc::new(InputSnapshot {
mouse_pos: vec2(50, 10),
mouse_delta: vec2(5, 0),
mouse_down: MouseButton::LEFT,
mouse_pressed: MouseButton::LEFT,
text_input,
..Default::default()
});
let mut ctx = WidgetCtx::new(
slider_id,
rect,
&mut commands,
&mut triangle_vertices,
&mut clip_stack,
&style,
&atlas,
&mut focus,
&mut updated_focus,
true,
Some(input),
);
let control = ControlState {
hovered: true,
focused: true,
clicked: false,
active: true,
scroll_delta: None,
};
let res = slider.run(&mut ctx, &control);
assert!(res.is_active());
assert!(slider.value.is_finite());
assert_eq!(slider.value, 5.0);
assert_eq!(slider.value, 5.0);
}
#[test]
fn slider_uses_widget_local_mouse_position() {
let atlas = make_test_atlas();
let style = Style::default();
let mut commands = Vec::new();
let mut triangle_vertices = Vec::new();
let mut clip_stack = Vec::new();
let mut focus = None;
let mut updated_focus = false;
let mut slider = Slider::new(0.0, 0.0, 100.0);
let slider_id = widget_id_of(&slider);
let rect = rect(40, 20, 100, 20);
let input = Rc::new(InputSnapshot {
mouse_pos: vec2(90, 30),
mouse_delta: vec2(0, 0),
mouse_down: MouseButton::LEFT,
mouse_pressed: MouseButton::LEFT,
..Default::default()
});
let mut ctx = WidgetCtx::new(
slider_id,
rect,
&mut commands,
&mut triangle_vertices,
&mut clip_stack,
&style,
&atlas,
&mut focus,
&mut updated_focus,
true,
Some(input),
);
let control = ControlState {
hovered: true,
focused: true,
clicked: false,
active: true,
scroll_delta: None,
};
let res = slider.run(&mut ctx, &control);
assert!(!res.is_none());
assert_eq!(slider.value, 50.0);
}
}