#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use std::io::{self, Write};
use crate::ported::crt::{
self, ColorElements, ColorScheme, ERR, KEY_CTRL, KEY_DOWN, KEY_END, KEY_HOME, KEY_LEFT,
KEY_MAX, KEY_NPAGE, KEY_PPAGE, KEY_RIGHT, KEY_UP, KEY_WHEELDOWN, KEY_WHEELUP,
};
use crate::ported::functionbar::{FunctionBar, FunctionBar_delete, FunctionBar_draw, Ncurses};
use crate::ported::listitem::ListItem;
use crate::ported::object::Object;
use crate::ported::richstring::{
RichString, RichString_rewind, RichString_setAttr, RichString_size, RichString_sizeVal,
RichString_writeWide,
};
use crate::ported::vector::{Vector, Vector_get, Vector_size};
use std::sync::atomic::Ordering;
const CTRL_N: i32 = KEY_CTRL(b'N' as i32);
const CTRL_P: i32 = KEY_CTRL(b'P' as i32);
const CTRL_B: i32 = KEY_CTRL(b'B' as i32);
const CTRL_F: i32 = KEY_CTRL(b'F' as i32);
const CTRL_A: i32 = KEY_CTRL(b'A' as i32);
const CTRL_E: i32 = KEY_CTRL(b'E' as i32);
const CARET: i32 = b'^' as i32;
const DOLLAR: i32 = b'$' as i32;
const KEY_SR: i32 = 0o521; const KEY_SF: i32 = 0o520;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct HandlerResult(pub u32);
impl HandlerResult {
pub const HANDLED: HandlerResult = HandlerResult(0x01);
pub const IGNORED: HandlerResult = HandlerResult(0x02);
pub const BREAK_LOOP: HandlerResult = HandlerResult(0x04);
pub const REFRESH: HandlerResult = HandlerResult(0x08);
pub const REDRAW: HandlerResult = HandlerResult(0x10);
pub const RESCAN: HandlerResult = HandlerResult(0x20);
pub const RESIZE: HandlerResult = HandlerResult(0x40);
pub const SYNTH_KEY: HandlerResult = HandlerResult(0x80);
pub fn contains(self, flag: HandlerResult) -> bool {
self.0 & flag.0 != 0
}
}
impl core::ops::BitOr for HandlerResult {
type Output = HandlerResult;
fn bitor(self, rhs: HandlerResult) -> HandlerResult {
HandlerResult(self.0 | rhs.0)
}
}
impl core::ops::BitOrAssign for HandlerResult {
fn bitor_assign(&mut self, rhs: HandlerResult) {
self.0 |= rhs.0;
}
}
impl core::ops::BitAnd for HandlerResult {
type Output = HandlerResult;
fn bitand(self, rhs: HandlerResult) -> HandlerResult {
HandlerResult(self.0 & rhs.0)
}
}
pub const EVENT_SET_SELECTED: i32 = -1;
pub const EVENT_PANEL_LOST_FOCUS: i32 = -2;
pub const fn EVENT_HEADER_CLICK(x_: i32) -> i32 {
-10000 + x_
}
pub const fn EVENT_IS_HEADER_CLICK(ev_: i32) -> bool {
ev_ >= -10000 && ev_ <= -9000
}
pub const fn EVENT_HEADER_CLICK_GET_X(ev_: i32) -> i32 {
ev_ + 10000
}
pub const fn EVENT_SCREEN_TAB_CLICK(x_: i32) -> i32 {
-20000 + x_
}
pub const fn EVENT_IS_SCREEN_TAB_CLICK(ev_: i32) -> bool {
ev_ >= -20000 && ev_ < -10000
}
pub const fn EVENT_SCREEN_TAB_GET_X(ev_: i32) -> i32 {
ev_ + 20000
}
pub const KEY_MOUSE_BAR_CLICK: i32 = KEY_MAX + 50;
pub enum PanelItem {
Owned(Box<dyn Object>),
Borrowed(*mut dyn Object),
}
impl PanelItem {
#[inline]
pub fn object(&self) -> &dyn Object {
match self {
PanelItem::Owned(b) => b.as_ref(),
PanelItem::Borrowed(p) => unsafe { &**p },
}
}
#[inline]
pub fn object_mut(&mut self) -> &mut dyn Object {
match self {
PanelItem::Owned(b) => b.as_mut(),
PanelItem::Borrowed(p) => unsafe { &mut **p },
}
}
}
pub struct Panel {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
pub cursorX: i32,
pub cursorY: i32,
pub items: Vec<PanelItem>,
pub selected: i32,
pub oldSelected: i32,
pub prevSelected: i32,
pub selectedLen: usize,
pub eventHandlerState: Option<Vec<u8>>,
pub scrollV: i32,
pub scrollH: i32,
pub needsRedraw: bool,
pub cursorOn: bool,
pub wasFocus: bool,
pub allowExcessScrollV: bool,
pub lastMouseBarClickX: i32,
pub currentBar: Option<FunctionBar>,
pub defaultBar: Option<FunctionBar>,
pub header: RichString,
pub selectionColorId: ColorElements,
}
impl Panel {
fn empty() -> Panel {
Panel {
x: 0,
y: 0,
w: 0,
h: 0,
cursorX: 0,
cursorY: 0,
items: Vec::new(),
selected: 0,
oldSelected: 0,
prevSelected: -1,
selectedLen: 0,
eventHandlerState: None,
scrollV: 0,
scrollH: 0,
needsRedraw: true,
cursorOn: false,
wasFocus: false,
allowExcessScrollV: false,
lastMouseBarClickX: 0,
currentBar: None,
defaultBar: None,
header: RichString::new(),
selectionColorId: ColorElements::PANEL_SELECTION_FOCUS,
}
}
}
pub trait PanelClass {
fn as_panel(&self) -> &Panel;
fn as_panel_mut(&mut self) -> &mut Panel;
fn event_handler(&mut self, _ev: i32) -> HandlerResult {
HandlerResult::IGNORED
}
fn draw_function_bar(&mut self, _hide_function_bar: bool) {}
fn print_header(&mut self) {}
}
impl PanelClass for Panel {
fn as_panel(&self) -> &Panel {
self
}
fn as_panel_mut(&mut self) -> &mut Panel {
self
}
fn event_handler(&mut self, ev: i32) -> HandlerResult {
Panel_selectByTyping(self, ev)
}
}
pub fn Panel_new(x: i32, y: i32, w: i32, h: i32, fuBar: Option<FunctionBar>) -> Panel {
let mut this = Panel::empty();
Panel_init(&mut this, x, y, w, h, fuBar);
this
}
pub fn Panel_delete(this: Panel) {
Panel_done(this);
}
pub fn Panel_init(this: &mut Panel, x: i32, y: i32, w: i32, h: i32, fuBar: Option<FunctionBar>) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.cursorX = 0;
this.cursorY = 0;
this.items = Vec::new();
this.scrollV = 0;
this.scrollH = 0;
this.selected = 0;
this.oldSelected = 0;
this.prevSelected = -1;
this.selectedLen = 0;
this.eventHandlerState = None; this.needsRedraw = true;
this.cursorOn = false;
this.wasFocus = false;
this.allowExcessScrollV = false; this.lastMouseBarClickX = 0;
this.header = RichString::new();
this.defaultBar = fuBar.clone();
this.currentBar = fuBar;
this.selectionColorId = ColorElements::PANEL_SELECTION_FOCUS;
}
pub fn Panel_setDefaultBar(this: &mut Panel) {
this.currentBar = this.defaultBar.clone();
}
pub fn Panel_done(this: Panel) {
let Panel {
defaultBar,
items,
eventHandlerState,
header,
..
} = this;
if let Some(bar) = defaultBar {
FunctionBar_delete(bar);
}
let _ = items;
let _ = eventHandlerState;
let _ = header;
}
pub fn Panel_setCursorToSelection(this: &mut Panel) {
this.cursorY = this.y + this.selected - this.scrollV + 1;
this.cursorX = this.x + (this.selectedLen as i32) - this.scrollH;
}
pub fn Panel_setSelectionColor(this: &mut Panel, colorId: ColorElements) {
this.selectionColorId = colorId;
}
pub fn Panel_setHeader(this: &mut Panel, header: &str) {
let attr = ColorElements::PANEL_HEADER_FOCUS.packed(ColorScheme::active());
RichString_writeWide(&mut this.header, attr, header.as_bytes());
this.needsRedraw = true;
}
pub fn Panel_move(this: &mut Panel, x: i32, y: i32) {
this.x = x;
this.y = y;
this.needsRedraw = true;
}
pub fn Panel_resize(this: &mut Panel, w: i32, h: i32) {
this.w = w;
this.h = h;
this.needsRedraw = true;
}
pub fn Panel_prune(this: &mut Panel) {
this.items.clear();
this.prevSelected = -1;
this.scrollV = 0;
this.selected = 0;
this.oldSelected = 0;
this.needsRedraw = true;
this.allowExcessScrollV = false; }
pub fn Panel_add(this: &mut Panel, o: Box<dyn Object>) {
this.items.push(PanelItem::Owned(o));
this.prevSelected = -1;
this.needsRedraw = true;
}
pub fn Panel_insert(this: &mut Panel, i: i32, o: Box<dyn Object>) {
this.items.insert(i as usize, PanelItem::Owned(o));
this.prevSelected = -1;
this.needsRedraw = true;
}
pub fn Panel_set(this: &mut Panel, i: i32, o: Box<dyn Object>) {
this.items[i as usize] = PanelItem::Owned(o);
}
pub fn Panel_get(this: &Panel, i: i32) -> &dyn Object {
this.items[i as usize].object()
}
pub fn Panel_remove(this: &mut Panel, i: i32) -> Box<dyn Object> {
this.needsRedraw = true;
let removed = match this.items.remove(i as usize) {
PanelItem::Owned(b) => b,
PanelItem::Borrowed(_) => panic!("Panel_remove on a borrowed item"),
};
this.prevSelected = -1;
if this.selected > 0 && this.selected >= this.items.len() as i32 {
this.selected -= 1;
}
removed
}
pub fn Panel_getSelected(this: &Panel) -> Option<&dyn Object> {
if !this.items.is_empty() {
Some(this.items[this.selected as usize].object())
} else {
None
}
}
pub fn Panel_moveSelectedUp(this: &mut Panel) {
let idx = this.selected;
if idx > 0 && (idx as usize) < this.items.len() {
this.items.swap(idx as usize, (idx - 1) as usize);
}
this.prevSelected = -1;
if this.selected > 0 {
this.selected -= 1;
}
}
pub fn Panel_moveSelectedDown(this: &mut Panel) {
let idx = this.selected;
let size = this.items.len() as i32;
if idx >= 0 && idx < size - 1 {
this.items.swap(idx as usize, (idx + 1) as usize);
}
this.prevSelected = -1;
if this.selected + 1 < size {
this.selected += 1;
}
}
pub fn Panel_getSelectedIndex(this: &Panel) -> i32 {
this.selected
}
pub fn Panel_size(this: &Panel) -> i32 {
this.items.len() as i32
}
pub fn Panel_setSelected(this: &mut Panel, selected: i32) {
let size = this.items.len() as i32;
let mut selected = selected;
if selected >= size {
selected = size - 1;
}
if selected < 0 {
selected = 0;
}
this.selected = selected;
}
pub fn Panel_splice(this: &mut Panel, from: &Vector) {
let n = Vector_size(from);
for i in 0..n {
let obj = Vector_get(from, i as usize) as *const dyn Object as *mut dyn Object;
this.items.push(PanelItem::Borrowed(obj));
}
this.prevSelected = -1;
this.needsRedraw = true;
}
pub fn Panel_draw(
this: &mut Panel,
force_redraw: bool,
focus: bool,
highlightSelected: bool,
hideFunctionBar: bool,
) {
let size = this.items.len() as i32;
let scrollH = this.scrollH;
let mut y = this.y;
let x = this.x;
let w = this.w;
let mut h = this.h;
if hideFunctionBar {
h += 1;
}
let header_attr = if focus {
ColorElements::PANEL_HEADER_FOCUS.packed(ColorScheme::active())
} else {
ColorElements::PANEL_HEADER_UNFOCUS.packed(ColorScheme::active())
};
let mut out = io::stdout().lock();
if force_redraw {
RichString_setAttr(&mut this.header, header_attr);
}
let header_len = RichString_sizeVal(&this.header);
if header_len > 0 {
Ncurses::attrset(&mut out, header_attr);
Ncurses::mvhline(&mut out, y, x, ' ', w);
if scrollH < header_len {
Panel::print_offset(
&mut out,
y,
x,
&this.header,
scrollH,
(header_len - scrollH).min(w),
);
}
Ncurses::attrset(
&mut out,
ColorElements::RESET_COLOR.packed(ColorScheme::active()),
);
y += 1;
h -= 1;
}
this.ensure_scroll(size, h);
let top_pad = if this.scrollV < 0 { -this.scrollV } else { 0 };
let first = this.scrollV + top_pad;
let up_to = (first + h - top_pad).min(size);
let selection_color = if focus {
this.selectionColorId.packed(ColorScheme::active())
} else {
ColorElements::PANEL_SELECTION_UNFOCUS.packed(ColorScheme::active())
};
let mut item = RichString::new();
if this.needsRedraw || force_redraw {
let mut line = 0i32;
while line < top_pad {
Ncurses::mvhline(&mut out, y + line, x, ' ', w);
line += 1;
}
let mut i = first;
while line < h && i < up_to {
let mut highlight_attr = 0i32;
{
let item_obj: &dyn Object = this.items[i as usize].object();
let sz = RichString_size(&item);
RichString_rewind(&mut item, sz);
item.highlightAttr = 0;
item_obj.display(&mut item);
}
let item_len = RichString_sizeVal(&item);
let amt = (item_len - scrollH).min(w);
if highlightSelected && i == this.selected {
item.highlightAttr = selection_color;
highlight_attr = selection_color;
}
if item.highlightAttr != 0 {
Ncurses::attrset(&mut out, item.highlightAttr);
let ha = item.highlightAttr;
RichString_setAttr(&mut item, ha);
this.selectedLen = item_len as usize;
highlight_attr = item.highlightAttr;
}
Ncurses::mvhline(&mut out, y + line, x, ' ', w);
if amt > 0 {
Panel::print_offset(&mut out, y + line, x, &item, scrollH, amt);
}
if highlight_attr != 0 {
Ncurses::attrset(
&mut out,
ColorElements::RESET_COLOR.packed(ColorScheme::active()),
);
}
line += 1;
i += 1;
}
while line < h {
Ncurses::mvhline(&mut out, y + line, x, ' ', w);
line += 1;
}
} else {
let scroll_v = this.scrollV;
let old_selected = this.oldSelected;
{
let old_obj: &dyn Object = this.items[old_selected as usize].object();
let sz = RichString_size(&item);
RichString_rewind(&mut item, sz);
old_obj.display(&mut item);
}
let old_len = RichString_sizeVal(&item);
Ncurses::mvhline(&mut out, y + old_selected - scroll_v, x, ' ', w);
if scrollH < old_len {
Panel::print_offset(
&mut out,
y + old_selected - scroll_v,
x,
&item,
scrollH,
(old_len - scrollH).min(w),
);
}
let selected = this.selected;
{
let new_obj: &dyn Object = this.items[selected as usize].object();
let sz = RichString_size(&item);
RichString_rewind(&mut item, sz);
item.highlightAttr = 0;
new_obj.display(&mut item);
}
let new_len = RichString_sizeVal(&item);
this.selectedLen = new_len as usize;
Ncurses::attrset(&mut out, selection_color);
Ncurses::mvhline(&mut out, y + selected - scroll_v, x, ' ', w);
RichString_setAttr(&mut item, selection_color);
if scrollH < new_len {
Panel::print_offset(
&mut out,
y + selected - scroll_v,
x,
&item,
scrollH,
(new_len - scrollH).min(w),
);
}
Ncurses::attrset(
&mut out,
ColorElements::RESET_COLOR.packed(ColorScheme::active()),
);
}
if focus && (this.needsRedraw || force_redraw || !this.wasFocus) {
if !hideFunctionBar {
if let Some(bar) = &this.currentBar {
FunctionBar_draw(bar);
}
}
}
let _ = out.flush();
this.oldSelected = this.selected;
this.wasFocus = focus;
this.needsRedraw = false;
}
pub fn Panel_headerHeight(this: &Panel) -> i32 {
if RichString_sizeVal(&this.header) > 0 {
1
} else {
0
}
}
pub fn Panel_onKey(this: &mut Panel, key: i32) -> bool {
let size = this.items.len() as i32;
let available_height = this.h - Panel_headerHeight(this); let max_scroll = (size - available_height).max(0); let scroll_h_amount = crt::CRT_scrollHAmount.load(Ordering::Relaxed);
match key {
KEY_DOWN | CTRL_N => {
this.selected += 1;
}
KEY_UP | CTRL_P => {
this.selected -= 1;
}
KEY_LEFT | CTRL_B => {
if this.scrollH > 0 {
this.scrollH -= scroll_h_amount.max(0);
this.needsRedraw = true;
}
}
KEY_RIGHT | CTRL_F => {
this.scrollH += scroll_h_amount;
this.needsRedraw = true;
}
KEY_PPAGE => {
let amt = this.h - Panel_headerHeight(this);
this.panel_scroll(-amt, size);
}
KEY_NPAGE => {
let amt = this.h - Panel_headerHeight(this);
this.panel_scroll(amt, size);
}
KEY_WHEELUP => {
let amt = crt::CRT_scrollWheelVAmount.load(Ordering::Relaxed);
this.panel_scroll(-amt, size);
}
KEY_WHEELDOWN => {
let amt = crt::CRT_scrollWheelVAmount.load(Ordering::Relaxed);
this.panel_scroll(amt, size);
}
KEY_SR => {
if this.scrollV > 0 {
if this.selected < this.scrollV + available_height {
this.scrollV -= 1;
this.needsRedraw = true;
}
}
}
KEY_SF => {
if this.scrollV < max_scroll {
if this.selected >= this.scrollV {
this.scrollV += 1;
this.needsRedraw = true;
}
}
}
KEY_HOME => {
this.selected = 0;
}
KEY_END => {
this.selected = size - 1;
}
CTRL_A | CARET => {
this.scrollH = 0;
this.needsRedraw = true;
}
CTRL_E | DOLLAR => {
debug_assert!(this.w > 0);
if this.selectedLen < this.w as usize {
this.scrollH = 0;
} else if this.selectedLen - (this.w as usize) > i32::MAX as usize {
this.scrollH = i32::MAX;
} else {
this.scrollH = (this.selectedLen - (this.w as usize)) as i32;
}
this.needsRedraw = true;
}
_ => return false,
}
if this.selected < 0 || size == 0 {
this.selected = 0;
this.needsRedraw = true;
} else if this.selected >= size {
this.selected = size - 1;
this.needsRedraw = true;
}
true
}
pub fn Panel_selectByTyping(this: &mut Panel, ch: i32) -> HandlerResult {
let size = Panel_size(this);
if ch == '#' as i32 {
return HandlerResult::IGNORED;
}
if this.eventHandlerState.is_none() {
this.eventHandlerState = Some(vec![0u8; 100]); }
let mut buffer = this.eventHandlerState.take().unwrap();
let mut ch = ch;
let strlen = |b: &[u8]| b.iter().position(|&c| c == 0).unwrap_or(b.len());
let result = 'done: {
if 0 < ch && ch < 255 && (ch as u8).is_ascii_graphic() {
let mut len = strlen(&buffer);
if len == 0 {
if ch == '/' as i32 {
ch = 0x01; } else if ch == 'q' as i32 {
break 'done HandlerResult::BREAK_LOOP;
}
} else if len == 1 && buffer[0] == 0x01 {
len -= 1;
}
if len < 99 {
buffer[len] = ch as u8;
buffer[len + 1] = b'\0';
}
for _try in 0..2 {
len = strlen(&buffer);
for i in 0..size {
let matched = {
let obj = Panel_get(this, i);
let any: &dyn core::any::Any = obj;
let li = any
.downcast_ref::<ListItem>()
.expect("Panel_selectByTyping: panel item is not a ListItem");
let val = li.value.as_bytes();
let start = val.iter().position(|&c| c != b' ').unwrap_or(val.len());
let cur = &val[start..];
cur.len() >= len && cur[..len].eq_ignore_ascii_case(&buffer[..len])
};
if matched {
Panel_setSelected(this, i);
break 'done HandlerResult::HANDLED;
}
}
buffer[0] = ch as u8;
buffer[1] = b'\0';
}
break 'done HandlerResult::HANDLED;
} else if ch != ERR {
buffer[0] = b'\0';
}
if ch == 13 {
break 'done HandlerResult::BREAK_LOOP;
}
HandlerResult::IGNORED
};
this.eventHandlerState = Some(buffer);
result
}
pub fn Panel_getCh(this: &Panel) -> i32 {
let mut out = io::stdout().lock();
if this.cursorOn {
Ncurses::move_to(&mut out, this.cursorY, this.cursorX);
Ncurses::curs_set(&mut out, true);
} else {
Ncurses::curs_set(&mut out, false);
}
let _ = out.flush();
crt::CRT_readKey()
}
impl Panel {
fn ensure_scroll(&mut self, size: i32, h: i32) {
if !self.allowExcessScrollV {
if self.scrollV < 0 {
self.scrollV = 0;
self.needsRedraw = true;
} else if self.scrollV > size - h {
self.scrollV = (size - h).max(0);
self.needsRedraw = true;
}
}
if self.selected < self.scrollV {
self.scrollV = self.selected;
self.needsRedraw = true;
} else if self.selected >= self.scrollV + h {
self.scrollV = self.selected - h + 1;
self.needsRedraw = true;
}
}
fn panel_scroll(&mut self, amount: i32, size: i32) {
self.selected += amount;
let hi = (size - self.h - Panel_headerHeight(self)).max(0);
self.scrollV = (self.scrollV + amount).clamp(0, hi);
self.needsRedraw = true;
}
fn print_offset<W: Write>(out: &mut W, y: i32, x: i32, item: &RichString, off: i32, n: i32) {
for k in 0..n {
let idx = (off + k) as usize;
if idx >= item.chptr.len() {
break;
}
let cell = item.chptr[idx];
Ncurses::attrset(out, cell.attr);
Ncurses::mvaddch(out, y, x + k, cell.chars);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::listitem::ListItem;
fn blank() -> Panel {
Panel::empty()
}
fn li(value: &str) -> Box<dyn Object> {
Box::new(ListItem {
value: value.to_string(),
key: 0,
moving: false,
})
}
fn fill(p: &mut Panel, n: usize) {
for i in 0..n {
p.items.push(PanelItem::Owned(li(&format!("item{i}"))));
}
}
#[test]
fn set_cursor_to_selection_matches_c_arithmetic() {
let mut p = blank();
p.x = 3;
p.y = 5;
p.selected = 7;
p.scrollV = 2;
p.scrollH = 4;
p.selectedLen = 20;
Panel_setCursorToSelection(&mut p);
assert_eq!(p.cursorY, 11); assert_eq!(p.cursorX, 19); }
#[test]
fn set_cursor_to_selection_truncates_selectedlen_like_int_cast() {
let mut p = blank();
p.selectedLen = 0x1_0000_0007;
Panel_setCursorToSelection(&mut p);
assert_eq!(p.cursorX, 7);
}
#[test]
fn set_selection_color_stores_value() {
let mut p = blank();
Panel_setSelectionColor(&mut p, ColorElements::PANEL_SELECTION_FOLLOW);
assert_eq!(p.selectionColorId, ColorElements::PANEL_SELECTION_FOLLOW);
Panel_setSelectionColor(&mut p, ColorElements::PANEL_SELECTION_UNFOCUS);
assert_eq!(p.selectionColorId, ColorElements::PANEL_SELECTION_UNFOCUS);
}
#[test]
fn move_sets_position_and_dirties() {
let mut p = blank();
p.needsRedraw = false;
Panel_move(&mut p, 12, 34);
assert_eq!((p.x, p.y), (12, 34));
assert!(p.needsRedraw);
}
#[test]
fn resize_sets_dimensions_and_dirties() {
let mut p = blank();
p.needsRedraw = false;
Panel_resize(&mut p, 80, 24);
assert_eq!((p.w, p.h), (80, 24));
assert!(p.needsRedraw);
}
#[test]
fn get_selected_index_returns_field() {
let mut p = blank();
p.selected = 9;
assert_eq!(Panel_getSelectedIndex(&p), 9);
}
#[test]
fn init_sets_all_fields() {
let mut p = blank();
p.selected = 5;
p.scrollV = 3;
let bar = FunctionBar {
functions: vec!["x".into()],
keys: vec!["F1".into()],
events: vec![1],
staticData: false,
};
Panel_init(&mut p, 1, 2, 3, 4, Some(bar));
assert_eq!((p.x, p.y, p.w, p.h), (1, 2, 3, 4));
assert_eq!(p.selected, 0);
assert_eq!(p.scrollV, 0);
assert_eq!(p.prevSelected, -1);
assert!(p.needsRedraw);
assert!(p.items.is_empty());
assert_eq!(p.selectionColorId, ColorElements::PANEL_SELECTION_FOCUS);
assert!(p.currentBar.is_some());
assert!(p.defaultBar.is_some());
}
#[test]
fn new_builds_initialized_panel() {
let p = Panel_new(0, 0, 10, 5, None);
assert_eq!((p.w, p.h), (10, 5));
assert!(p.items.is_empty());
assert!(p.needsRedraw);
}
#[test]
fn set_default_bar_restores_current_from_default() {
let default_bar = FunctionBar {
functions: vec!["DEFAULT".into()],
keys: vec!["F1".into()],
events: vec![1],
staticData: false,
};
let mut p = Panel_new(0, 0, 10, 5, Some(default_bar));
p.currentBar = Some(FunctionBar {
functions: vec!["SEARCH".into()],
keys: vec!["Esc".into()],
events: vec![2],
staticData: false,
});
Panel_setDefaultBar(&mut p);
assert_eq!(
p.currentBar.as_ref().unwrap().functions,
vec!["DEFAULT".to_string()]
);
assert!(p.defaultBar.is_some());
}
#[test]
fn add_and_size_and_get() {
let mut p = blank();
Panel_add(&mut p, li("a"));
Panel_add(&mut p, li("b"));
assert_eq!(Panel_size(&p), 2);
assert_eq!(p.prevSelected, -1);
let any: &dyn std::any::Any = Panel_get(&p, 1);
assert_eq!(any.downcast_ref::<ListItem>().unwrap().value, "b");
}
#[test]
fn insert_and_set() {
let mut p = blank();
Panel_add(&mut p, li("a"));
Panel_add(&mut p, li("c"));
Panel_insert(&mut p, 1, li("b"));
assert_eq!(Panel_size(&p), 3);
let any: &dyn std::any::Any = Panel_get(&p, 1);
assert_eq!(any.downcast_ref::<ListItem>().unwrap().value, "b");
Panel_set(&mut p, 0, li("Z"));
let any0: &dyn std::any::Any = Panel_get(&p, 0);
assert_eq!(any0.downcast_ref::<ListItem>().unwrap().value, "Z");
}
#[test]
fn remove_decrements_selected_when_at_end() {
let mut p = blank();
fill(&mut p, 3); p.selected = 2;
let removed = Panel_remove(&mut p, 2);
let any: &dyn std::any::Any = removed.as_ref();
assert_eq!(any.downcast_ref::<ListItem>().unwrap().value, "item2");
assert_eq!(Panel_size(&p), 2);
assert_eq!(p.selected, 1);
}
#[test]
fn remove_keeps_selected_when_in_range() {
let mut p = blank();
fill(&mut p, 3);
p.selected = 0;
Panel_remove(&mut p, 2);
assert_eq!(p.selected, 0);
}
#[test]
fn get_selected_empty_is_none() {
let p = blank();
assert!(Panel_getSelected(&p).is_none());
}
#[test]
fn get_selected_returns_current() {
let mut p = blank();
fill(&mut p, 3);
p.selected = 1;
let sel: &dyn std::any::Any = Panel_getSelected(&p).unwrap();
assert_eq!(sel.downcast_ref::<ListItem>().unwrap().value, "item1");
}
#[test]
fn move_selected_up_swaps_and_decrements() {
let mut p = blank();
fill(&mut p, 3); p.selected = 2;
Panel_moveSelectedUp(&mut p);
let a1: &dyn std::any::Any = Panel_get(&p, 1);
let a2: &dyn std::any::Any = Panel_get(&p, 2);
assert_eq!(a1.downcast_ref::<ListItem>().unwrap().value, "item2");
assert_eq!(a2.downcast_ref::<ListItem>().unwrap().value, "item1");
assert_eq!(p.selected, 1);
}
#[test]
fn move_selected_up_at_top_is_noop() {
let mut p = blank();
fill(&mut p, 3);
p.selected = 0;
Panel_moveSelectedUp(&mut p);
let a0: &dyn std::any::Any = Panel_get(&p, 0);
assert_eq!(a0.downcast_ref::<ListItem>().unwrap().value, "item0");
assert_eq!(p.selected, 0);
}
#[test]
fn move_selected_down_swaps_and_increments() {
let mut p = blank();
fill(&mut p, 3);
p.selected = 0;
Panel_moveSelectedDown(&mut p);
let a0: &dyn std::any::Any = Panel_get(&p, 0);
let a1: &dyn std::any::Any = Panel_get(&p, 1);
assert_eq!(a0.downcast_ref::<ListItem>().unwrap().value, "item1");
assert_eq!(a1.downcast_ref::<ListItem>().unwrap().value, "item0");
assert_eq!(p.selected, 1);
}
#[test]
fn move_selected_down_at_bottom_is_noop() {
let mut p = blank();
fill(&mut p, 3);
p.selected = 2;
Panel_moveSelectedDown(&mut p);
let a2: &dyn std::any::Any = Panel_get(&p, 2);
assert_eq!(a2.downcast_ref::<ListItem>().unwrap().value, "item2");
assert_eq!(p.selected, 2);
}
#[test]
fn prune_clears_and_resets() {
let mut p = blank();
fill(&mut p, 4);
p.selected = 3;
p.scrollV = 2;
Panel_prune(&mut p);
assert_eq!(Panel_size(&p), 0);
assert_eq!(p.selected, 0);
assert_eq!(p.scrollV, 0);
assert_eq!(p.oldSelected, 0);
assert_eq!(p.prevSelected, -1);
assert!(p.needsRedraw);
}
#[test]
fn set_selected_clamps_high_low_and_empty() {
let mut p = blank();
fill(&mut p, 5); Panel_setSelected(&mut p, 10);
assert_eq!(p.selected, 4); Panel_setSelected(&mut p, -3);
assert_eq!(p.selected, 0); Panel_setSelected(&mut p, 2);
assert_eq!(p.selected, 2);
let mut empty = blank();
Panel_setSelected(&mut empty, 4);
assert_eq!(empty.selected, 0);
}
#[test]
fn header_height_reflects_header_content() {
let mut p = blank();
assert_eq!(Panel_headerHeight(&p), 0);
Panel_setHeader(&mut p, "PID USER");
assert_eq!(Panel_headerHeight(&p), 1);
assert!(p.needsRedraw);
assert_eq!(RichString_sizeVal(&p.header), "PID USER".len() as i32);
}
#[test]
fn onkey_down_up_move_selection_within_bounds() {
let mut p = blank();
fill(&mut p, 3);
p.h = 3;
assert!(Panel_onKey(&mut p, KEY_DOWN));
assert_eq!(p.selected, 1);
assert!(Panel_onKey(&mut p, KEY_DOWN));
assert_eq!(p.selected, 2);
assert!(Panel_onKey(&mut p, KEY_DOWN));
assert_eq!(p.selected, 2);
assert!(Panel_onKey(&mut p, KEY_UP));
assert_eq!(p.selected, 1);
}
#[test]
fn onkey_up_at_top_clamps_to_zero() {
let mut p = blank();
fill(&mut p, 3);
p.selected = 0;
assert!(Panel_onKey(&mut p, KEY_UP)); assert_eq!(p.selected, 0);
}
#[test]
fn onkey_home_end() {
let mut p = blank();
fill(&mut p, 5);
p.selected = 2;
assert!(Panel_onKey(&mut p, KEY_END));
assert_eq!(p.selected, 4);
assert!(Panel_onKey(&mut p, KEY_HOME));
assert_eq!(p.selected, 0);
}
#[test]
fn onkey_left_right_scroll_horizontal() {
let mut p = blank();
fill(&mut p, 3);
let step = crt::CRT_scrollHAmount.load(Ordering::Relaxed);
assert!(Panel_onKey(&mut p, KEY_RIGHT));
assert_eq!(p.scrollH, step);
assert!(Panel_onKey(&mut p, KEY_LEFT));
assert_eq!(p.scrollH, 0);
assert!(Panel_onKey(&mut p, KEY_LEFT));
assert_eq!(p.scrollH, 0);
}
#[test]
fn onkey_caret_resets_scrollh_and_dollar_scrolls_to_end() {
let mut p = blank();
fill(&mut p, 3);
p.w = 10;
p.scrollH = 7;
assert!(Panel_onKey(&mut p, CARET));
assert_eq!(p.scrollH, 0);
p.selectedLen = 25;
assert!(Panel_onKey(&mut p, DOLLAR));
assert_eq!(p.scrollH, 15);
p.selectedLen = 4;
assert!(Panel_onKey(&mut p, DOLLAR));
assert_eq!(p.scrollH, 0);
}
#[test]
fn onkey_page_down_up_scrolls_by_page() {
let mut p = blank();
fill(&mut p, 20);
p.h = 5; assert!(Panel_onKey(&mut p, KEY_NPAGE));
assert_eq!(p.selected, 5);
assert_eq!(p.scrollV, 5);
assert!(Panel_onKey(&mut p, KEY_PPAGE));
assert_eq!(p.selected, 0);
assert_eq!(p.scrollV, 0);
}
#[test]
fn onkey_wheel_down_up_scrolls_by_wheel_amount() {
let mut p = blank();
fill(&mut p, 40);
p.h = 5; let amt = crt::CRT_scrollWheelVAmount.load(Ordering::Relaxed);
assert!(Panel_onKey(&mut p, KEY_WHEELDOWN));
assert_eq!(p.selected, amt);
assert_eq!(p.scrollV, amt);
assert!(Panel_onKey(&mut p, KEY_WHEELUP));
assert_eq!(p.selected, 0);
assert_eq!(p.scrollV, 0);
}
#[test]
fn onkey_shift_up_scrolls_one_line_without_moving_selection() {
let mut p = blank();
fill(&mut p, 20);
p.h = 5; p.scrollV = 5;
p.selected = 5;
assert!(Panel_onKey(&mut p, KEY_SR));
assert_eq!(p.scrollV, 4);
assert_eq!(p.selected, 5);
p.scrollV = 0;
assert!(Panel_onKey(&mut p, KEY_SR));
assert_eq!(p.scrollV, 0);
}
#[test]
fn onkey_shift_down_scrolls_one_line_without_moving_selection() {
let mut p = blank();
fill(&mut p, 20);
p.h = 5; p.scrollV = 0;
p.selected = 0;
assert!(Panel_onKey(&mut p, KEY_SF));
assert_eq!(p.scrollV, 1);
assert_eq!(p.selected, 0);
p.scrollV = 15;
assert!(Panel_onKey(&mut p, KEY_SF));
assert_eq!(p.scrollV, 15);
}
#[test]
fn onkey_unhandled_returns_false() {
let mut p = blank();
fill(&mut p, 3);
assert!(!Panel_onKey(&mut p, b'z' as i32));
}
#[test]
fn ensure_scroll_pulls_selection_into_view_downward() {
let mut p = blank();
fill(&mut p, 20);
p.h = 5;
p.selected = 10;
p.scrollV = 0;
p.ensure_scroll(20, 5);
assert_eq!(p.scrollV, 6);
assert!(p.needsRedraw);
}
#[test]
fn ensure_scroll_pulls_selection_into_view_upward() {
let mut p = blank();
fill(&mut p, 20);
p.h = 5;
p.selected = 2;
p.scrollV = 8;
p.ensure_scroll(20, 5);
assert_eq!(p.scrollV, 2);
}
#[test]
fn ensure_scroll_clamps_negative_and_overshoot() {
let mut p = blank();
fill(&mut p, 4);
p.h = 10;
p.selected = 0;
p.scrollV = -3;
p.ensure_scroll(4, 10);
assert_eq!(p.scrollV, 0);
p.scrollV = 5;
p.ensure_scroll(4, 10);
assert_eq!(p.scrollV, 0);
}
fn with_items(values: &[&str]) -> Panel {
let mut p = blank();
for v in values {
p.items.push(PanelItem::Owned(li(v)));
}
p
}
fn search_buf(p: &Panel) -> String {
let b = p.eventHandlerState.as_ref().expect("buffer not allocated");
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
String::from_utf8(b[..end].to_vec()).unwrap()
}
#[test]
fn typing_selects_first_matching_prefix() {
let mut p = with_items(&["apple", "banana", "cherry"]);
let r = Panel_selectByTyping(&mut p, 'b' as i32);
assert_eq!(r, HandlerResult::HANDLED);
assert_eq!(p.selected, 1); assert_eq!(search_buf(&p), "b");
}
#[test]
fn typing_narrows_selection_as_chars_accumulate() {
let mut p = with_items(&["bee", "banana", "bat"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'b' as i32),
HandlerResult::HANDLED
);
assert_eq!(p.selected, 0);
assert_eq!(
Panel_selectByTyping(&mut p, 'a' as i32),
HandlerResult::HANDLED
);
assert_eq!(p.selected, 1);
assert_eq!(
Panel_selectByTyping(&mut p, 't' as i32),
HandlerResult::HANDLED
);
assert_eq!(p.selected, 2);
assert_eq!(search_buf(&p), "bat");
}
#[test]
fn typing_is_case_insensitive() {
let mut p = with_items(&["apple", "Banana", "cherry"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'B' as i32),
HandlerResult::HANDLED
);
assert_eq!(p.selected, 1);
let mut q = with_items(&["apple", "Banana", "cherry"]);
assert_eq!(
Panel_selectByTyping(&mut q, 'b' as i32),
HandlerResult::HANDLED
);
assert_eq!(q.selected, 1);
}
#[test]
fn no_match_keeps_selection_and_returns_handled() {
let mut p = with_items(&["apple", "banana"]);
p.selected = 1;
let r = Panel_selectByTyping(&mut p, 'z' as i32);
assert_eq!(r, HandlerResult::HANDLED);
assert_eq!(p.selected, 1); assert_eq!(search_buf(&p), "z");
}
#[test]
fn leading_spaces_are_skipped_before_matching() {
let mut p = with_items(&[" indented", "other"]);
let r = Panel_selectByTyping(&mut p, 'i' as i32);
assert_eq!(r, HandlerResult::HANDLED);
assert_eq!(p.selected, 0); }
#[test]
fn retry_treats_last_char_as_start_of_new_word() {
let mut p = with_items(&["apple", "xray"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'z' as i32),
HandlerResult::HANDLED
);
assert_eq!(p.selected, 0);
assert_eq!(
Panel_selectByTyping(&mut p, 'x' as i32),
HandlerResult::HANDLED
);
assert_eq!(p.selected, 1);
assert_eq!(search_buf(&p), "x");
}
#[test]
fn hash_is_ignored_and_leaves_buffer_unallocated() {
let mut p = with_items(&["apple"]);
let r = Panel_selectByTyping(&mut p, '#' as i32);
assert_eq!(r, HandlerResult::IGNORED);
assert!(p.eventHandlerState.is_none());
}
#[test]
fn q_on_empty_buffer_breaks_loop() {
let mut p = with_items(&["apple", "banana"]);
let r = Panel_selectByTyping(&mut p, 'q' as i32);
assert_eq!(r, HandlerResult::BREAK_LOOP);
assert_eq!(search_buf(&p), "");
}
#[test]
fn q_after_text_is_a_normal_search_char() {
let mut p = with_items(&["apple", "aqua"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'a' as i32),
HandlerResult::HANDLED
);
let r = Panel_selectByTyping(&mut p, 'q' as i32);
assert_eq!(r, HandlerResult::HANDLED);
assert_eq!(p.selected, 1);
assert_eq!(search_buf(&p), "aq");
}
#[test]
fn slash_marker_is_dropped_when_next_char_arrives() {
let mut p = with_items(&["apple"]);
let r = Panel_selectByTyping(&mut p, '/' as i32);
assert_eq!(r, HandlerResult::HANDLED);
assert_eq!(p.eventHandlerState.as_ref().unwrap()[0], 0x01);
let r = Panel_selectByTyping(&mut p, 'a' as i32);
assert_eq!(r, HandlerResult::HANDLED);
assert_eq!(p.selected, 0); assert_eq!(search_buf(&p), "a");
}
#[test]
fn nongraphic_char_clears_buffer_and_returns_ignored() {
let mut p = with_items(&["apple", "banana"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'b' as i32),
HandlerResult::HANDLED
);
assert_eq!(search_buf(&p), "b");
let r = Panel_selectByTyping(&mut p, 0x08);
assert_eq!(r, HandlerResult::IGNORED);
assert_eq!(search_buf(&p), "");
}
#[test]
fn enter_clears_buffer_and_breaks_loop() {
let mut p = with_items(&["apple"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'a' as i32),
HandlerResult::HANDLED
);
let r = Panel_selectByTyping(&mut p, 13);
assert_eq!(r, HandlerResult::BREAK_LOOP);
assert_eq!(search_buf(&p), ""); }
#[test]
fn err_is_ignored_and_leaves_buffer_intact() {
let mut p = with_items(&["apple"]);
assert_eq!(
Panel_selectByTyping(&mut p, 'a' as i32),
HandlerResult::HANDLED
);
let r = Panel_selectByTyping(&mut p, ERR);
assert_eq!(r, HandlerResult::IGNORED);
assert_eq!(search_buf(&p), "a"); }
}