use crate::widgets::controls::slider::{Slider, SliderOptions, SliderOrientation};
use crate::widgets::scroll::{ScrollOrientation, ScrollState};
use crate::widgets::{Widget, WidgetArea, WindowView};
use crate::{Color, ColorPair, Event, MouseButton, Result, Window};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScrollUnit {
Absolute,
Viewport,
Content,
Step,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArrowDirection {
Up,
Down,
Left,
Right,
}
pub struct ArrowButton {
width: u16,
height: u16,
dir: ArrowDirection,
color: ColorPair,
}
impl ArrowButton {
pub fn new(dir: ArrowDirection) -> Self {
Self {
width: 1,
height: 1,
dir,
color: ColorPair::new(Color::White, Color::Transparent),
}
}
pub fn with_color(mut self, color: ColorPair) -> Self {
self.color = color;
self
}
pub fn set_color(&mut self, color: ColorPair) {
self.color = color;
}
pub fn glyph(&self) -> &'static str {
match self.dir {
ArrowDirection::Up => "▲",
ArrowDirection::Down => "▼",
ArrowDirection::Left => "◀",
ArrowDirection::Right => "▶",
}
}
}
impl Widget for ArrowButton {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
let x = self.width.saturating_sub(1) / 2;
let y = self.height.saturating_sub(1) / 2;
window.write_str_colored(y, x, self.glyph(), self.color)?;
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(0, 0)
}
}
#[derive(Debug, Clone)]
pub struct ScrollBarOptions {
pub orientation: ScrollOrientation,
pub show_arrows: bool,
pub track_color: ColorPair,
pub thumb_color: ColorPair,
pub arrow_color: ColorPair,
pub scroll_step: Option<u16>,
}
impl Default for ScrollBarOptions {
fn default() -> Self {
Self {
orientation: ScrollOrientation::Vertical,
show_arrows: false,
track_color: ColorPair::new(Color::DarkGray, Color::Transparent),
thumb_color: ColorPair::new(Color::White, Color::Transparent),
arrow_color: ColorPair::new(Color::White, Color::Transparent),
scroll_step: None,
}
}
}
pub struct ScrollBar {
width: u16,
height: u16,
opts: ScrollBarOptions,
state: Rc<RefCell<ScrollState>>,
slider: Slider,
start_arrow: ArrowButton,
end_arrow: ArrowButton,
synced: bool,
}
impl ScrollBar {
pub fn new(
width: u16,
height: u16,
state: Rc<RefCell<ScrollState>>,
opts: ScrollBarOptions,
) -> Self {
let (start_dir, end_dir, slider_orientation) = match opts.orientation {
ScrollOrientation::Vertical => (
ArrowDirection::Up,
ArrowDirection::Down,
SliderOrientation::Vertical,
),
ScrollOrientation::Horizontal => (
ArrowDirection::Left,
ArrowDirection::Right,
SliderOrientation::Horizontal,
),
};
let mut slider = Slider::new(
width,
height,
SliderOptions {
orientation: slider_orientation,
min: 0.0,
max: 0.0,
value: 0.0,
viewport_size: 1.0,
track_color: opts.track_color,
thumb_color: opts.thumb_color,
min_thumb_virtual: 1,
},
);
slider = slider.with_colors(opts.track_color, opts.thumb_color);
let mut this = Self {
width,
height,
opts: opts.clone(),
state,
slider,
start_arrow: ArrowButton::new(start_dir).with_color(opts.arrow_color),
end_arrow: ArrowButton::new(end_dir).with_color(opts.arrow_color),
synced: false,
};
this.sync_from_state_and_resize_parts();
this
}
pub fn is_dragging(&self) -> bool {
self.slider.is_dragging()
}
pub fn vertical(height: u16, state: Rc<RefCell<ScrollState>>) -> Self {
Self::new(
1,
height,
state,
ScrollBarOptions {
orientation: ScrollOrientation::Vertical,
..ScrollBarOptions::default()
},
)
}
pub fn horizontal(width: u16, state: Rc<RefCell<ScrollState>>) -> Self {
Self::new(
width,
1,
state,
ScrollBarOptions {
orientation: ScrollOrientation::Horizontal,
..ScrollBarOptions::default()
},
)
}
pub fn with_size(mut self, width: u16, height: u16) -> Self {
self.width = width;
self.height = height;
self.sync_from_state_and_resize_parts();
self
}
pub fn set_size(&mut self, width: u16, height: u16) {
self.width = width;
self.height = height;
self.sync_from_state_and_resize_parts();
}
pub fn with_show_arrows(mut self, show: bool) -> Self {
self.opts.show_arrows = show;
self.sync_from_state_and_resize_parts();
self
}
pub fn set_show_arrows(&mut self, show: bool) {
self.opts.show_arrows = show;
self.sync_from_state_and_resize_parts();
}
pub fn with_colors(mut self, track: ColorPair, thumb: ColorPair) -> Self {
self.opts.track_color = track;
self.opts.thumb_color = thumb;
self.slider.set_colors(track, thumb);
self.synced = false;
self
}
pub fn set_colors(&mut self, track: ColorPair, thumb: ColorPair) {
self.opts.track_color = track;
self.opts.thumb_color = thumb;
self.slider.set_colors(track, thumb);
self.synced = false;
}
pub fn with_arrow_color(mut self, color: ColorPair) -> Self {
self.opts.arrow_color = color;
self.start_arrow.set_color(color);
self.end_arrow.set_color(color);
self.synced = false;
self
}
pub fn set_arrow_color(&mut self, color: ColorPair) {
self.opts.arrow_color = color;
self.start_arrow.set_color(color);
self.end_arrow.set_color(color);
self.synced = false;
}
pub fn with_scroll_step(mut self, step: Option<u16>) -> Self {
self.opts.scroll_step = step;
self.synced = false;
self
}
pub fn set_scroll_step(&mut self, step: Option<u16>) {
self.opts.scroll_step = step;
self.synced = false;
}
pub fn scroll_state(&self) -> Rc<RefCell<ScrollState>> {
self.state.clone()
}
#[allow(dead_code)]
fn track_len_cells(&self) -> u16 {
let axis = match self.opts.orientation {
ScrollOrientation::Vertical => self.height,
ScrollOrientation::Horizontal => self.width,
};
if self.opts.show_arrows {
axis.saturating_sub(2) } else {
axis
}
}
fn slider_area_within(&self, outer: WidgetArea) -> WidgetArea {
match self.opts.orientation {
ScrollOrientation::Vertical => {
let top = if self.opts.show_arrows { 1 } else { 0 };
let h = outer
.height
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 });
WidgetArea::new(outer.x, outer.y + top, outer.width, h)
}
ScrollOrientation::Horizontal => {
let left = if self.opts.show_arrows { 1 } else { 0 };
let w = outer
.width
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 });
WidgetArea::new(outer.x + left, outer.y, w, outer.height)
}
}
}
fn start_arrow_area_within(&self, outer: WidgetArea) -> WidgetArea {
match self.opts.orientation {
ScrollOrientation::Vertical => WidgetArea::new(outer.x, outer.y, outer.width, 1),
ScrollOrientation::Horizontal => WidgetArea::new(outer.x, outer.y, 1, outer.height),
}
}
fn end_arrow_area_within(&self, outer: WidgetArea) -> WidgetArea {
match self.opts.orientation {
ScrollOrientation::Vertical => {
let y = outer.y + outer.height.saturating_sub(1);
WidgetArea::new(outer.x, y, outer.width, 1)
}
ScrollOrientation::Horizontal => {
let x = outer.x + outer.width.saturating_sub(1);
WidgetArea::new(x, outer.y, 1, outer.height)
}
}
}
pub fn sync_from_state_and_resize_parts(&mut self) {
match self.opts.orientation {
ScrollOrientation::Vertical => {
self.start_arrow.width = self.width.max(1);
self.start_arrow.height = 1;
self.end_arrow.width = self.width.max(1);
self.end_arrow.height = 1;
}
ScrollOrientation::Horizontal => {
self.start_arrow.width = 1;
self.start_arrow.height = self.height.max(1);
self.end_arrow.width = 1;
self.end_arrow.height = self.height.max(1);
}
}
let slider_w;
let slider_h;
match self.opts.orientation {
ScrollOrientation::Vertical => {
slider_w = self.width.max(1);
slider_h = self
.height
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
}
ScrollOrientation::Horizontal => {
slider_w = self
.width
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 });
slider_h = self.height.max(1);
}
}
let slider_w = slider_w.max(1);
let slider_h = slider_h.max(1);
let s = *self.state.borrow();
let max_offset = s.max_offset_for(self.opts.orientation);
let viewport_size = match self.opts.orientation {
ScrollOrientation::Vertical => s.viewport_size().height,
ScrollOrientation::Horizontal => s.viewport_size().width,
};
let offset = s.offset_for(self.opts.orientation);
self.slider.set_size(slider_w, slider_h);
self.slider.set_range(0.0, max_offset as f32);
self.slider.set_viewport_size(viewport_size.max(1) as f32);
self.slider.set_value(offset as f32);
self.synced = true;
}
fn sync_state_from_slider(&mut self) {
let value = self.slider.value().round().max(0.0);
let value = value as u16;
self.state
.borrow_mut()
.set_offset_for(self.opts.orientation, value);
self.sync_from_state_and_resize_parts();
}
pub fn scroll_by_cells(&mut self, delta: i16) {
self.state
.borrow_mut()
.scroll_by(self.opts.orientation, delta);
self.sync_from_state_and_resize_parts();
}
pub fn scroll_by_fraction(&mut self, delta_fraction: f32, unit: ScrollUnit) {
let s = *self.state.borrow();
let multiplier: u16 = match unit {
ScrollUnit::Absolute => 1,
ScrollUnit::Viewport => match self.opts.orientation {
ScrollOrientation::Vertical => s.viewport_size().height,
ScrollOrientation::Horizontal => s.viewport_size().width,
},
ScrollUnit::Content => match self.opts.orientation {
ScrollOrientation::Vertical => s.content_size().height,
ScrollOrientation::Horizontal => s.content_size().width,
},
ScrollUnit::Step => self.opts.scroll_step.unwrap_or(1),
}
.max(1);
let delta = (multiplier as f32 * delta_fraction).round() as i16;
self.scroll_by_cells(delta);
}
pub fn handle_event(&mut self, event: &Event, area: WidgetArea) -> bool {
self.sync_from_state_and_resize_parts();
if self.opts.show_arrows {
match *event {
Event::MouseClick {
x,
y,
button: MouseButton::Left,
} => {
let start_area = self.start_arrow_area_within(area);
if start_area.contains_point(x, y) {
let step = self.opts.scroll_step.unwrap_or(1) as i16;
self.scroll_by_cells(-step);
return true;
}
let end_area = self.end_arrow_area_within(area);
if end_area.contains_point(x, y) {
let step = self.opts.scroll_step.unwrap_or(1) as i16;
self.scroll_by_cells(step);
return true;
}
}
_ => {}
}
}
let slider_area = self.slider_area_within(area);
let slider_changed = self.slider.handle_event(event, slider_area);
if slider_changed {
self.sync_state_from_slider();
return true;
}
match *event {
Event::KeyUp | Event::Character('k') => {
if self.opts.orientation == ScrollOrientation::Vertical {
self.scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport);
return true;
}
}
Event::KeyDown | Event::Character('j') => {
if self.opts.orientation == ScrollOrientation::Vertical {
self.scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport);
return true;
}
}
Event::KeyLeft | Event::Character('h') => {
if self.opts.orientation == ScrollOrientation::Horizontal {
self.scroll_by_fraction(-1.0 / 5.0, ScrollUnit::Viewport);
return true;
}
}
Event::KeyRight | Event::Character('l') => {
if self.opts.orientation == ScrollOrientation::Horizontal {
self.scroll_by_fraction(1.0 / 5.0, ScrollUnit::Viewport);
return true;
}
}
_ => {}
}
false
}
#[allow(dead_code)]
fn draw_vertical(&self, window: &mut dyn Window) -> Result<()> {
if self.opts.show_arrows {
{
let mut view = WindowView {
window,
x_offset: 0,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: 1,
};
self.start_arrow.draw(&mut view)?;
}
{
let y = self.height.saturating_sub(1);
let mut view = WindowView {
window,
x_offset: 0,
y_offset: y,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: 1,
};
self.end_arrow.draw(&mut view)?;
}
}
{
let top = if self.opts.show_arrows { 1 } else { 0 };
let h = self
.height
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
let mut view = WindowView {
window,
x_offset: 0,
y_offset: top,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: h,
};
self.slider.draw(&mut view)?;
}
Ok(())
}
#[allow(dead_code)]
fn draw_horizontal(&self, window: &mut dyn Window) -> Result<()> {
if self.opts.show_arrows {
{
let mut view = WindowView {
window,
x_offset: 0,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: self.height.max(1),
};
self.start_arrow.draw(&mut view)?;
}
{
let x = self.width.saturating_sub(1);
let mut view = WindowView {
window,
x_offset: x,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: self.height.max(1),
};
self.end_arrow.draw(&mut view)?;
}
}
{
let left = if self.opts.show_arrows { 1 } else { 0 };
let w = self
.width
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
let mut view = WindowView {
window,
x_offset: left,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: w,
height: self.height.max(1),
};
self.slider.draw(&mut view)?;
}
Ok(())
}
}
impl Widget for ScrollBar {
fn draw(&self, window: &mut dyn Window) -> Result<()> {
if self.synced {
match self.opts.orientation {
ScrollOrientation::Vertical => {
if self.opts.show_arrows {
{
let mut view = WindowView {
window,
x_offset: 0,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: 1,
};
self.start_arrow.draw(&mut view)?;
}
{
let y = self.height.saturating_sub(1);
let mut view = WindowView {
window,
x_offset: 0,
y_offset: y,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: 1,
};
self.end_arrow.draw(&mut view)?;
}
}
let top = if self.opts.show_arrows { 1 } else { 0 };
let h = self
.height
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
let mut view = WindowView {
window,
x_offset: 0,
y_offset: top,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: h,
};
self.slider.draw(&mut view)?;
}
ScrollOrientation::Horizontal => {
if self.opts.show_arrows {
{
let mut view = WindowView {
window,
x_offset: 0,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: self.height.max(1),
};
self.start_arrow.draw(&mut view)?;
}
{
let x = self.width.saturating_sub(1);
let mut view = WindowView {
window,
x_offset: x,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: self.height.max(1),
};
self.end_arrow.draw(&mut view)?;
}
}
let left = if self.opts.show_arrows { 1 } else { 0 };
let w = self
.width
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
let mut view = WindowView {
window,
x_offset: left,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: w,
height: self.height.max(1),
};
self.slider.draw(&mut view)?;
}
}
return Ok(());
}
let s = *self.state.borrow();
let max_offset = s.max_offset_for(self.opts.orientation);
let viewport_size = match self.opts.orientation {
ScrollOrientation::Vertical => s.viewport_size().height,
ScrollOrientation::Horizontal => s.viewport_size().width,
};
let offset = s.offset_for(self.opts.orientation);
let slider_orientation = match self.opts.orientation {
ScrollOrientation::Vertical => SliderOrientation::Vertical,
ScrollOrientation::Horizontal => SliderOrientation::Horizontal,
};
let slider_w;
let slider_h;
match self.opts.orientation {
ScrollOrientation::Vertical => {
slider_w = self.width.max(1);
slider_h = self
.height
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
}
ScrollOrientation::Horizontal => {
slider_w = self
.width
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
slider_h = self.height.max(1);
}
}
let render_slider = Slider::new(
slider_w,
slider_h,
SliderOptions {
orientation: slider_orientation,
min: 0.0,
max: max_offset as f32,
value: offset as f32,
viewport_size: viewport_size.max(1) as f32,
track_color: self.opts.track_color,
thumb_color: self.opts.thumb_color,
min_thumb_virtual: 1,
},
);
let start_arrow = match self.opts.orientation {
ScrollOrientation::Vertical => ArrowButton {
width: self.width.max(1),
height: 1,
dir: ArrowDirection::Up,
color: self.opts.arrow_color,
},
ScrollOrientation::Horizontal => ArrowButton {
width: 1,
height: self.height.max(1),
dir: ArrowDirection::Left,
color: self.opts.arrow_color,
},
};
let end_arrow = match self.opts.orientation {
ScrollOrientation::Vertical => ArrowButton {
width: self.width.max(1),
height: 1,
dir: ArrowDirection::Down,
color: self.opts.arrow_color,
},
ScrollOrientation::Horizontal => ArrowButton {
width: 1,
height: self.height.max(1),
dir: ArrowDirection::Right,
color: self.opts.arrow_color,
},
};
match self.opts.orientation {
ScrollOrientation::Vertical => {
if self.opts.show_arrows {
{
let mut view = WindowView {
window,
x_offset: 0,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: 1,
};
start_arrow.draw(&mut view)?;
}
{
let y = self.height.saturating_sub(1);
let mut view = WindowView {
window,
x_offset: 0,
y_offset: y,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: 1,
};
end_arrow.draw(&mut view)?;
}
}
let top = if self.opts.show_arrows { 1 } else { 0 };
let h = self
.height
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
let mut view = WindowView {
window,
x_offset: 0,
y_offset: top,
scroll_x: 0,
scroll_y: 0,
width: self.width.max(1),
height: h,
};
render_slider.draw(&mut view)?;
}
ScrollOrientation::Horizontal => {
if self.opts.show_arrows {
{
let mut view = WindowView {
window,
x_offset: 0,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: self.height.max(1),
};
start_arrow.draw(&mut view)?;
}
{
let x = self.width.saturating_sub(1);
let mut view = WindowView {
window,
x_offset: x,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: 1,
height: self.height.max(1),
};
end_arrow.draw(&mut view)?;
}
}
let left = if self.opts.show_arrows { 1 } else { 0 };
let w = self
.width
.saturating_sub(if self.opts.show_arrows { 2 } else { 0 })
.max(1);
let mut view = WindowView {
window,
x_offset: left,
y_offset: 0,
scroll_x: 0,
scroll_y: 0,
width: w,
height: self.height.max(1),
};
render_slider.draw(&mut view)?;
}
}
Ok(())
}
fn get_size(&self) -> (u16, u16) {
(self.width, self.height)
}
fn get_position(&self) -> (u16, u16) {
(0, 0)
}
}