use super::view::{View, write_line_to_terminal};
use crate::core::draw::DrawBuffer;
use crate::core::event::{
Event, EventType, KB_DOWN, KB_END, KB_HOME, KB_LEFT, KB_PGDN, KB_PGUP, KB_RIGHT, KB_UP,
MB_LEFT_BUTTON,
};
use crate::core::geometry::{Point, Rect};
use crate::core::palette::{SCROLLBAR_INDICATOR, SCROLLBAR_PAGE};
use crate::terminal::Terminal;
const SB_INDICATOR: i16 = 0;
const SB_UP_ARROW: i16 = 1;
const SB_DOWN_ARROW: i16 = 2;
const SB_PAGE_UP: i16 = 3;
const SB_PAGE_DOWN: i16 = 4;
pub const VSCROLL_CHARS: [char; 5] = [
'â–ˆ', 'â–²', 'â–¼', 'â–‘', 'â–‘', ];
pub const HSCROLL_CHARS: [char; 5] = [
'â–ˆ', 'â—„', 'â–º', 'â–‘', 'â–‘', ];
pub struct ScrollBar {
bounds: Rect,
value: i32,
min_val: i32,
max_val: i32,
pg_step: i32, ar_step: i32, total: i32, chars: [char; 5],
is_vertical: bool,
palette_chain: Option<crate::core::palette_chain::PaletteChainNode>,
dragging_thumb: bool,
}
impl ScrollBar {
pub fn new_vertical(bounds: Rect) -> Self {
Self {
bounds,
value: 0,
min_val: 0,
max_val: 0,
pg_step: 1,
ar_step: 1,
total: 0,
chars: VSCROLL_CHARS,
is_vertical: true,
palette_chain: None,
dragging_thumb: false,
}
}
pub fn new_horizontal(bounds: Rect) -> Self {
Self {
bounds,
value: 0,
min_val: 0,
max_val: 0,
pg_step: 1,
ar_step: 1,
total: 0,
chars: HSCROLL_CHARS,
is_vertical: false,
palette_chain: None,
dragging_thumb: false,
}
}
pub fn set_params(
&mut self,
value: i32,
min_val: i32,
max_val: i32,
pg_step: i32,
ar_step: i32,
) {
self.min_val = min_val;
self.max_val = max_val.max(min_val);
self.value = value.max(self.min_val).min(self.max_val);
self.pg_step = pg_step;
self.ar_step = ar_step;
}
pub fn set_value(&mut self, value: i32) {
self.value = value.max(self.min_val).min(self.max_val);
}
pub fn set_range(&mut self, min_val: i32, max_val: i32) {
self.min_val = min_val;
self.max_val = max_val;
self.value = self.value.max(min_val).min(max_val);
}
pub fn get_value(&self) -> i32 {
self.value
}
pub fn set_total(&mut self, total: i32) {
self.total = total.max(0);
}
fn get_size(&self) -> i32 {
if self.is_vertical {
(self.bounds.height() - 2).max(1) as i32
} else {
(self.bounds.width() - 2).max(1) as i32
}
}
fn get_thumb_size(&self) -> i32 {
let track = self.get_size();
if self.total <= 0 || self.max_val <= self.min_val {
return 0;
}
let thumb = (track as i64 / self.total as i64) as i32;
thumb.max(1).min(track)
}
fn get_pos(&self) -> i32 {
let track = self.get_size();
let thumb = self.get_thumb_size();
let usable = track - thumb;
let range = self.max_val - self.min_val;
if range <= 0 || usable <= 0 {
0
} else {
((self.value - self.min_val) as i64 * usable as i64 / range as i64)
.max(0)
.min(usable as i64) as i32
}
}
#[expect(
dead_code,
reason = "Borland TV API - reserved for advanced scrollbar interaction"
)]
fn get_part_at(&self, p: Point) -> i16 {
let rel_x = p.x - self.bounds.a.x;
let rel_y = p.y - self.bounds.a.y;
if self.is_vertical {
if rel_y == 0 {
SB_UP_ARROW
} else if rel_y == self.bounds.height() - 1 {
SB_DOWN_ARROW
} else {
let pos = self.get_pos();
if rel_y - 1 == pos as i16 {
SB_INDICATOR
} else if rel_y - 1 < pos as i16 {
SB_PAGE_UP
} else {
SB_PAGE_DOWN
}
}
} else if rel_x == 0 {
SB_UP_ARROW } else if rel_x == self.bounds.width() - 1 {
SB_DOWN_ARROW } else {
let pos = self.get_pos();
if rel_x - 1 == pos as i16 {
SB_INDICATOR
} else if rel_x - 1 < pos as i16 {
SB_PAGE_UP } else {
SB_PAGE_DOWN }
}
}
#[expect(
dead_code,
reason = "Borland TV API - reserved for advanced scrollbar interaction"
)]
fn scroll_step(&mut self, part: i16) -> i32 {
match part {
SB_UP_ARROW => -self.ar_step,
SB_DOWN_ARROW => self.ar_step,
SB_PAGE_UP => -self.pg_step,
SB_PAGE_DOWN => self.pg_step,
_ => 0,
}
}
fn keypress(&mut self, event: &mut Event) {
if self.is_vertical {
match event.key_code {
KB_UP => {
self.value = (self.value - self.ar_step).max(self.min_val);
event.clear();
}
KB_DOWN => {
self.value = (self.value + self.ar_step).min(self.max_val);
event.clear();
}
KB_PGUP => {
self.value = (self.value - self.pg_step).max(self.min_val);
event.clear();
}
KB_PGDN => {
self.value = (self.value + self.pg_step).min(self.max_val);
event.clear();
}
KB_HOME => {
self.value = self.min_val;
event.clear();
}
KB_END => {
self.value = self.max_val;
event.clear();
}
_ => {}
}
} else {
match event.key_code {
KB_LEFT => {
self.value = (self.value - self.ar_step).max(self.min_val);
event.clear();
}
KB_RIGHT => {
self.value = (self.value + self.ar_step).min(self.max_val);
event.clear();
}
KB_HOME => {
self.value = self.min_val;
event.clear();
}
KB_END => {
self.value = self.max_val;
event.clear();
}
_ => {}
}
}
}
fn drag(&mut self, event: &mut Event) {
if self.is_vertical {
let mouse_y = event.mouse.pos.y;
let rel_y = (mouse_y - self.bounds.a.y - 1) as i32; let range = self.max_val - self.min_val + 1;
let s = self.get_size();
log::debug!(
"Dragging thumb to rel_y: {}, size: {}, range: {}, max_val: {}, min_val: {}",
rel_y,
s,
range,
self.max_val,
self.min_val
);
if s > 0 && range > 0 {
let new_pos = rel_y.max(0).min(s);
log::debug!("Calculated new thumb position: {}", new_pos);
self.value = (new_pos * range / s) + self.min_val;
}
} else {
let mouse_x = event.mouse.pos.x;
let rel_x = (mouse_x - self.bounds.a.x - 1) as i32; let range = self.max_val - self.min_val + 1;
let s = self.get_size();
log::debug!(
"Dragging thumb to rel_x: {}, size: {}, range: {}, max_val: {}, min_val: {}",
rel_x,
s,
range,
self.max_val,
self.min_val
);
if s > 0 && range > 0 {
let new_pos = rel_x.max(0).min(s);
log::debug!("Calculated new thumb position: {}", new_pos);
self.value = (new_pos * range / s) + self.min_val;
}
}
}
fn left_click(&mut self, event: &mut Event) {
let mouse_pos = event.mouse.pos;
if self.is_vertical {
if mouse_pos.x >= self.bounds.a.x
&& mouse_pos.x < self.bounds.b.x
&& mouse_pos.y >= self.bounds.a.y
&& mouse_pos.y < self.bounds.b.y
{
let rel_y = mouse_pos.y - self.bounds.a.y;
let height = self.bounds.height();
if rel_y == 0 {
self.value = (self.value - self.ar_step).max(self.min_val);
event.clear();
} else if rel_y == height - 1 {
self.value = (self.value + self.ar_step).min(self.max_val);
event.clear();
} else {
let range = self.max_val - self.min_val;
if range > 0 {
let thumb_pos = self.get_pos() as i16;
let thumb_sz = self.get_thumb_size() as i16;
let track_y = rel_y - 1;
if track_y < thumb_pos {
self.value = (self.value - self.pg_step).max(self.min_val);
} else if track_y >= thumb_pos + thumb_sz {
self.value = (self.value + self.pg_step).min(self.max_val);
} else {
self.dragging_thumb = true;
}
event.clear();
}
}
}
} else {
if mouse_pos.y >= self.bounds.a.y
&& mouse_pos.y < self.bounds.b.y
&& mouse_pos.x >= self.bounds.a.x
&& mouse_pos.x < self.bounds.b.x
{
let rel_x = mouse_pos.x - self.bounds.a.x;
let width = self.bounds.width();
if rel_x == 0 {
self.value = (self.value - self.ar_step).max(self.min_val);
event.clear();
} else if rel_x == width - 1 {
self.value = (self.value + self.ar_step).min(self.max_val);
event.clear();
} else {
let range = self.max_val - self.min_val;
if range > 0 {
let thumb_pos = self.get_pos() as i16;
let thumb_sz = self.get_thumb_size() as i16;
let track_x = rel_x - 1;
if track_x < thumb_pos {
self.value = (self.value - self.pg_step).max(self.min_val);
} else if track_x >= thumb_pos + thumb_sz {
self.value = (self.value + self.pg_step).min(self.max_val);
} else {
self.dragging_thumb = true;
}
event.clear();
}
}
}
}
}
}
impl View for ScrollBar {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
fn draw(&mut self, terminal: &mut Terminal) {
let page_attr = self.map_color(SCROLLBAR_PAGE);
let indicator_attr = self.map_color(SCROLLBAR_INDICATOR);
let pos = self.get_pos();
let thumb = self.get_thumb_size();
let thumb_start = pos as i16;
let thumb_end = thumb_start + thumb as i16;
if self.is_vertical {
let height = self.bounds.height();
for y in 0..height {
let mut buf = DrawBuffer::new(1);
let track_pos = y - 1;
let in_thumb =
track_pos >= thumb_start && track_pos < thumb_end && y > 0 && y < height - 1;
let ch = if y == 0 {
self.chars[1]
} else if y == height - 1 {
self.chars[2]
} else if in_thumb {
self.chars[0]
} else {
self.chars[3]
};
let attr = if in_thumb { indicator_attr } else { page_attr };
buf.put_char(0, ch, attr);
write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y + y, &buf);
}
} else {
let width = self.bounds.width();
let mut buf = DrawBuffer::new(width as usize);
for x in 0..width {
let track_pos = x - 1;
let in_thumb =
track_pos >= thumb_start && track_pos < thumb_end && x > 0 && x < width - 1;
let ch = if x == 0 {
self.chars[1]
} else if x == width - 1 {
self.chars[2]
} else if in_thumb {
self.chars[0]
} else {
self.chars[3]
};
let attr = if in_thumb { indicator_attr } else { page_attr };
buf.put_char(x as usize, ch, attr);
}
write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y, &buf);
}
}
fn handle_event(&mut self, event: &mut Event) {
log::debug!("ScrollBar received event: {:?}", event);
match event.what {
EventType::MouseMove => {
if self.dragging_thumb {
self.drag(event);
event.clear();
}
}
EventType::Keyboard => {
self.keypress(event);
}
EventType::MouseDown => {
if (event.mouse.buttons & MB_LEFT_BUTTON) != 0 {
self.left_click(event);
}
}
EventType::MouseUp => {
if self.dragging_thumb {
self.dragging_thumb = false;
event.clear();
}
}
_ => {}
}
}
fn set_palette_chain(&mut self, node: Option<crate::core::palette_chain::PaletteChainNode>) {
self.palette_chain = node;
}
fn get_palette_chain(&self) -> Option<&crate::core::palette_chain::PaletteChainNode> {
self.palette_chain.as_ref()
}
fn get_palette(&self) -> Option<crate::core::palette::Palette> {
use crate::core::palette::{Palette, palettes};
Some(Palette::from_slice(palettes::CP_SCROLLBAR))
}
}
pub struct ScrollBarBuilder {
bounds: Option<Rect>,
is_vertical: bool,
value: i32,
min_val: i32,
max_val: i32,
pg_step: i32,
ar_step: i32,
}
impl ScrollBarBuilder {
pub fn new() -> Self {
Self {
bounds: None,
is_vertical: true,
value: 0,
min_val: 0,
max_val: 0,
pg_step: 1,
ar_step: 1,
}
}
#[must_use]
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
#[must_use]
pub fn vertical(mut self) -> Self {
self.is_vertical = true;
self
}
#[must_use]
pub fn horizontal(mut self) -> Self {
self.is_vertical = false;
self
}
#[must_use]
pub fn params(
mut self,
value: i32,
min_val: i32,
max_val: i32,
pg_step: i32,
ar_step: i32,
) -> Self {
self.value = value;
self.min_val = min_val;
self.max_val = max_val;
self.pg_step = pg_step;
self.ar_step = ar_step;
self
}
#[must_use]
pub fn value(mut self, value: i32) -> Self {
self.value = value;
self
}
#[must_use]
pub fn range(mut self, min_val: i32, max_val: i32) -> Self {
self.min_val = min_val;
self.max_val = max_val;
self
}
#[must_use]
pub fn page_step(mut self, pg_step: i32) -> Self {
self.pg_step = pg_step;
self
}
#[must_use]
pub fn arrow_step(mut self, ar_step: i32) -> Self {
self.ar_step = ar_step;
self
}
pub fn build(self) -> ScrollBar {
let bounds = self.bounds.expect("ScrollBar bounds must be set");
let chars = if self.is_vertical {
VSCROLL_CHARS
} else {
HSCROLL_CHARS
};
ScrollBar {
bounds,
value: self
.value
.max(self.min_val)
.min(self.max_val.max(self.min_val)),
min_val: self.min_val,
max_val: self.max_val.max(self.min_val),
pg_step: self.pg_step,
ar_step: self.ar_step,
total: 0,
chars,
is_vertical: self.is_vertical,
palette_chain: None,
dragging_thumb: false,
}
}
pub fn build_boxed(self) -> Box<ScrollBar> {
Box::new(self.build())
}
}
impl Default for ScrollBarBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_thumb_when_nothing_to_scroll() {
let mut bar = ScrollBar::new_vertical(Rect::new(0, 0, 1, 12));
bar.set_params(0, 0, 0, 10, 1);
assert_eq!(bar.get_thumb_size(), 0);
bar.set_params(0, 0, 50, 10, 1);
bar.set_total(100);
assert!(bar.get_thumb_size() >= 1);
}
}