#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(dead_code)]
use std::io::Write;
use std::sync::atomic::Ordering;
use crate::ported::crt::{CRT_colorSchemes, CRT_utf8, ColorElements, ColorScheme};
use crate::ported::functionbar::Ncurses;
use crate::ported::listitem::{ListItem, ListItem_new};
use crate::ported::machine::Machine;
use crate::ported::object::{Object, ObjectClass, Object_class};
use crate::ported::richstring::{
RichString, RichString_appendChr, RichString_appendWide, RichString_delete,
RichString_getCharVal, RichString_printoffnVal, RichString_setAttrn, RichString_setChar,
RichString_sizeVal, RichString_writeWide,
};
const UNIT_PREFIXES: [char; 10] = ['K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'];
const ONE_K: f64 = 1024.0;
const DEFAULT_GRAPH_HEIGHT: i32 = 4;
pub fn Meter_humanUnit(mut value: f64) -> String {
let mut i: usize = 0;
while value >= ONE_K {
if i >= UNIT_PREFIXES.len() - 1 {
if value > 9999.0 {
return "inf".to_string();
}
break;
}
value /= ONE_K;
i += 1;
}
let mut precision = 0;
if i > 0 {
precision = if value <= 99.9 {
if value <= 9.99 {
2
} else {
1
}
} else {
0
};
if precision < 2 {
let limit = if precision == 1 { 10.0 } else { 100.0 };
if value < limit {
value = limit;
}
}
}
format!("{:.*}{}", precision, value, UNIT_PREFIXES[i])
}
pub type MeterModeId = u32;
pub const BAR_METERMODE: MeterModeId = 1;
pub const TEXT_METERMODE: MeterModeId = 2;
pub const GRAPH_METERMODE: MeterModeId = 3;
pub const LED_METERMODE: MeterModeId = 4;
pub const LAST_METERMODE: MeterModeId = 5;
pub const METERMODE_DEFAULT_SUPPORTED: u32 =
(1 << BAR_METERMODE) | (1 << TEXT_METERMODE) | (1 << GRAPH_METERMODE) | (1 << LED_METERMODE);
#[derive(Default)]
pub struct GraphData {
pub nValues: usize,
pub values: Vec<f64>,
pub time_ms: u64,
}
pub type MeterDraw = fn(&mut dyn Write, &mut Meter, i32, i32, i32);
pub type MeterInit = fn(&mut Meter);
pub type MeterDone = fn(&mut Meter);
pub type MeterUpdateMode = fn(&mut Meter, MeterModeId);
pub type MeterUpdateValues = fn(&mut Meter);
pub type MeterGetCaption = fn(&Meter) -> String;
pub type MeterGetUiName = fn(&Meter) -> String;
pub type MeterDisplay = fn(&Meter, &mut RichString);
pub struct MeterClass {
pub super_: ObjectClass,
pub display: Option<MeterDisplay>,
pub init: Option<MeterInit>,
pub done: Option<MeterDone>,
pub updateMode: Option<MeterUpdateMode>,
pub updateValues: Option<MeterUpdateValues>,
pub draw: Option<MeterDraw>,
pub getCaption: Option<MeterGetCaption>,
pub getUiName: Option<MeterGetUiName>,
pub defaultMode: MeterModeId,
pub supportedModes: u32,
pub total: f64,
pub attributes: &'static [i32],
pub name: &'static str,
pub uiName: &'static str,
pub caption: &'static str,
pub description: Option<&'static str>,
pub maxItems: u8,
pub isMultiColumn: bool,
pub isPercentChart: bool,
}
pub static Meter_class: MeterClass = MeterClass {
super_: ObjectClass {
extends: Some(&Object_class),
},
display: None,
init: None,
done: None,
updateMode: None,
updateValues: None,
draw: None,
getCaption: None,
getUiName: None,
defaultMode: 0,
supportedModes: 0,
total: 0.0,
attributes: &[],
name: "",
uiName: "",
caption: "",
description: None,
maxItems: 0,
isMultiColumn: false,
isPercentChart: false,
};
static BlankMeter_attributes: [i32; 1] = [ColorElements::DEFAULT_COLOR as i32];
pub static BlankMeter_class: MeterClass = MeterClass {
super_: ObjectClass {
extends: Some(&Meter_class.super_),
},
display: Some(BlankMeter_display),
init: None,
done: None,
updateMode: None,
updateValues: Some(BlankMeter_updateValues),
draw: None,
getCaption: None,
getUiName: None,
defaultMode: TEXT_METERMODE,
supportedModes: 1 << TEXT_METERMODE,
total: 0.0,
attributes: &BlankMeter_attributes,
name: "Blank",
uiName: "Blank",
caption: "",
description: None,
maxItems: 0,
isMultiColumn: false,
isPercentChart: false,
};
pub struct MeterMode {
pub draw: Option<MeterDraw>,
pub uiName: Option<&'static str>,
pub h: i32,
}
pub static Meter_modes: [MeterMode; LAST_METERMODE as usize] = [
MeterMode {
draw: None,
uiName: None,
h: 0,
},
MeterMode {
draw: Some(BarMeterMode_draw),
uiName: Some("Bar"),
h: 1,
},
MeterMode {
draw: Some(TextMeterMode_draw),
uiName: Some("Text"),
h: 1,
},
MeterMode {
draw: Some(GraphMeterMode_draw),
uiName: Some("Graph"),
h: DEFAULT_GRAPH_HEIGHT,
},
MeterMode {
draw: Some(LEDMeterMode_draw),
uiName: Some("LED"),
h: 3,
},
];
pub struct Meter {
pub values: Vec<f64>,
pub curItems: u8,
pub mode: MeterModeId,
pub supportedModes: u32,
pub caption: String,
pub param: u32,
pub drawData: GraphData,
pub h: i32,
pub curAttributes: Option<&'static [i32]>,
pub txtBuffer: String,
pub total: f64,
pub attributes: &'static [i32],
pub isPercentChart: bool,
pub columnWidthCount: i32,
pub isMultiColumn: bool,
pub name: &'static str,
pub uiName: &'static str,
pub getUiName: Option<MeterGetUiName>,
pub display: Option<MeterDisplay>,
pub updateValues: Option<MeterUpdateValues>,
pub init: Option<MeterInit>,
pub updateMode: Option<MeterUpdateMode>,
pub classDraw: Option<MeterDraw>,
pub draw: Option<MeterDraw>,
pub host: *const Machine,
pub meterData: Option<Box<dyn std::any::Any>>,
}
impl Meter {
pub(crate) fn empty() -> Meter {
Meter {
host: core::ptr::null(),
meterData: None,
name: "",
values: Vec::new(),
curItems: 0,
mode: 0,
supportedModes: 0,
caption: String::new(),
param: 0,
drawData: GraphData::default(),
h: 1,
curAttributes: None,
txtBuffer: String::new(),
total: 0.0,
attributes: &[],
isPercentChart: false,
columnWidthCount: 0,
isMultiColumn: false,
uiName: "",
getUiName: None,
display: None,
updateValues: None,
init: None,
updateMode: None,
classDraw: None,
draw: None,
}
}
fn crt_colors(idx: i32) -> i32 {
CRT_colorSchemes[ColorScheme::active() as usize][idx as usize]
}
}
impl Object for Meter {
fn klass(&self) -> &'static ObjectClass {
&Meter_class.super_
}
fn display(&self, out: &mut RichString) {
match self.display {
Some(display) => display(self, out),
None => unimplemented!(
"Object::display: Meter class has no display method (C NULL vtable slot)"
),
}
}
}
pub fn Meter_new(host: *const Machine, param: u32, type_: &'static MeterClass) -> Meter {
let mut this = Meter {
h: 1,
param,
host,
curItems: type_.maxItems,
curAttributes: None,
values: if type_.maxItems > 0 {
vec![0.0; type_.maxItems as usize]
} else {
Vec::new()
},
total: type_.total,
caption: type_.caption.to_string(),
supportedModes: type_.supportedModes,
attributes: type_.attributes,
isPercentChart: type_.isPercentChart,
columnWidthCount: 0,
isMultiColumn: type_.isMultiColumn,
name: type_.name,
uiName: type_.uiName,
getUiName: type_.getUiName,
display: type_.display,
updateValues: type_.updateValues,
init: type_.init,
updateMode: type_.updateMode,
classDraw: type_.draw,
..Meter::empty()
};
if let Some(init) = type_.init {
init(&mut this);
}
Meter_setMode(&mut this, type_.defaultMode);
debug_assert!(this.mode > 0);
this
}
pub fn Meter_computeSum(this: &Meter) -> f64 {
let sum = crate::ported::xutils::sumPositiveValues(&this.values[..this.curItems as usize]);
if f64::MAX < sum {
f64::MAX
} else {
sum
}
}
pub fn Meter_nextSupportedMode(this: &Meter) -> MeterModeId {
let supportedModes = this.supportedModes;
debug_assert!(supportedModes != 0);
debug_assert!(this.mode < 32);
let mode_mask = (u32::MAX << 1) << this.mode;
let mut next_modes = supportedModes & mode_mask;
if next_modes == 0 {
next_modes = supportedModes;
}
crate::ported::xutils::countTrailingZeros(next_modes) as MeterModeId
}
pub fn Meter_displayBuffer(this: &Meter, out: &mut RichString) {
if let Some(display) = this.display {
display(this, out);
} else {
RichString_writeWide(
out,
Meter::crt_colors(this.attributes[0]),
this.txtBuffer.as_bytes(),
);
}
}
pub fn TextMeterMode_draw(mut out: &mut dyn Write, this: &mut Meter, x: i32, y: i32, w: i32) {
let scheme = ColorScheme::active();
let caption = this.caption.clone();
if w > 0 {
Ncurses::attrset(&mut out, ColorElements::METER_TEXT.packed(scheme));
Ncurses::mvaddnstr(&mut out, y, x, &caption, w);
}
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
let caption_width = if w > 0 {
(caption.len() as i32).min(w)
} else {
0
};
if w <= caption_width {
return;
}
let w = w - caption_width;
let x = x + caption_width;
let mut text = RichString::new();
Meter_displayBuffer(this, &mut text);
RichString_printoffnVal(&mut out, &text, y, x, 0, w);
RichString_delete(&mut text);
}
const BarMeterMode_characters: &[u8] = b"|#*@$%&.";
pub fn BarMeterMode_draw(mut out: &mut dyn Write, this: &mut Meter, x: i32, y: i32, w: i32) {
let scheme = ColorScheme::active();
let mut x = x;
let mut w = w;
let caption_len = 3;
let caption = this.caption.clone();
if w >= caption_len {
Ncurses::attrset(&mut out, ColorElements::METER_TEXT.packed(scheme));
Ncurses::mvaddnstr(&mut out, y, x, &caption, caption_len);
}
w -= caption_len;
if w >= 1 {
x += caption_len;
Ncurses::attrset(&mut out, ColorElements::BAR_BORDER.packed(scheme));
Ncurses::mvaddch(&mut out, y, x, '[');
w -= 1;
Ncurses::mvaddch(&mut out, y, x + w, ']');
w -= 1;
}
if !this.isPercentChart && this.curItems > 0 {
let sum = Meter_computeSum(this);
this.total = if sum > this.total { sum } else { this.total };
}
if w < 1 {
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
return;
}
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme)); x += 1;
let mut bar = RichString::new();
RichString_appendChr(&mut bar, 0, ' ', w);
RichString_appendWide(&mut bar, 0, this.txtBuffer.as_bytes());
let mut start_pos = RichString_sizeVal(&bar) - w;
if start_pos > w {
let mut pos = 2 * w;
while pos > w {
if RichString_getCharVal(&bar, pos as usize) == ' ' {
while pos > w && RichString_getCharVal(&bar, (pos - 1) as usize) == ' ' {
pos -= 1;
}
start_pos = pos - w;
break;
}
pos -= 1;
}
start_pos = start_pos.min(w);
}
debug_assert!(start_pos >= 0);
debug_assert!(start_pos <= w);
debug_assert!(start_pos + w <= RichString_sizeVal(&bar));
let mut block_sizes = [0i32; 10];
let mut offset = 0i32;
for i in 0..this.curItems as usize {
let value = this.values[i];
if value > 0.0 && this.total > 0.0 {
let value = value.min(this.total);
let mut bs = ((value / this.total) * w as f64).ceil() as i32;
bs = bs.min(w - offset);
block_sizes[i] = bs;
} else {
block_sizes[i] = 0;
}
let next_offset = offset + block_sizes[i];
let mut j = offset;
while j < next_offset {
if RichString_getCharVal(&bar, (start_pos + j) as usize) == ' ' {
if scheme == ColorScheme::COLORSCHEME_MONOCHROME {
debug_assert!(i < BarMeterMode_characters.len());
RichString_setChar(
&mut bar,
(start_pos + j) as usize,
BarMeterMode_characters[i] as char,
);
} else {
RichString_setChar(&mut bar, (start_pos + j) as usize, '|');
}
}
j += 1;
}
offset = next_offset;
}
offset = 0;
for i in 0..this.curItems as usize {
let attr = match this.curAttributes {
Some(ca) => ca[i],
None => this.attributes[i],
};
RichString_setAttrn(
&mut bar,
Meter::crt_colors(attr),
(start_pos + offset) as usize,
block_sizes[i] as usize,
);
RichString_printoffnVal(
&mut out,
&bar,
y,
x + offset,
start_pos + offset,
block_sizes[i],
);
offset += block_sizes[i];
}
if offset < w {
RichString_setAttrn(
&mut bar,
ColorElements::BAR_SHADOW.packed(scheme),
(start_pos + offset) as usize,
(w - offset) as usize,
);
RichString_printoffnVal(
&mut out,
&bar,
y,
x + offset,
start_pos + offset,
w - offset,
);
}
RichString_delete(&mut bar);
Ncurses::move_to(&mut out, y, x + w + 1);
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
}
const MAX_METER_GRAPHDATA_VALUES: usize = 32768;
const PIXPERROW_ASCII: i32 = 2;
const PIXPERROW_UTF8: i32 = 4;
static GraphMeterMode_dotsAscii: [&str; 9] = [
" ", ".", ":", ".", ".", ":", ":", ":", ":",
];
static GraphMeterMode_dotsUtf8: [&str; 25] = [
" ", "⢀", "⢠", "⢰", "⢸", "⡀", "⣀", "⣠", "⣰", "⣸", "⡄", "⣄", "⣤", "⣴", "⣼", "⡆", "⣆", "⣦", "⣶", "⣾", "⡇", "⣇", "⣧", "⣷", "⣿",
];
pub fn GraphMeterMode_draw(mut out: &mut dyn Write, this: &mut Meter, x: i32, y: i32, w: i32) {
let scheme = ColorScheme::active();
let caption_len = 3;
let caption = this.caption.clone();
let mut x = x;
let mut w = w;
if w >= caption_len {
Ncurses::attrset(&mut out, ColorElements::METER_TEXT.packed(scheme));
Ncurses::mvaddnstr(&mut out, y, x, &caption, caption_len);
}
w -= caption_len;
debug_assert!(this.h >= 1);
let h = this.h;
let is_percent_chart = this.isPercentChart;
let old_n = this.drawData.nValues;
if w > (old_n / 2) as i32 && MAX_METER_GRAPHDATA_VALUES > old_n {
let mut new_n = (old_n + old_n / 2).max((w as usize) * 2);
new_n = new_n.min(MAX_METER_GRAPHDATA_VALUES);
let add = new_n - old_n;
let mut newv = vec![0.0f64; add];
newv.extend_from_slice(&this.drawData.values);
newv.resize(new_n, 0.0);
this.drawData.values = newv;
this.drawData.nValues = new_n;
}
let n_values = this.drawData.nValues;
if n_values < 1 {
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
return;
}
let host: &Machine = unsafe { &*this.host };
if host.realtimeMs >= this.drawData.time_ms {
let global_delay = host
.settings
.as_ref()
.expect("GraphMeterMode_draw: host->settings")
.delay;
this.drawData.time_ms = host.realtimeMs + (global_delay as u64) * 100;
this.drawData.values.copy_within(1..n_values, 0);
let last = n_values - 1;
let mut newval = 0.0;
if this.curItems > 0 {
newval = Meter_computeSum(this);
if is_percent_chart && this.total > 0.0 {
newval /= this.total;
}
}
this.drawData.values[last] = newval;
}
if w < 1 {
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
return;
}
x += caption_len;
let utf8 = CRT_utf8.load(Ordering::Relaxed);
let (dots, pix_per_row): (&[&str], i32) = if utf8 {
(&GraphMeterMode_dotsUtf8[..], PIXPERROW_UTF8)
} else {
(&GraphMeterMode_dotsAscii[..], PIXPERROW_ASCII)
};
if w as usize > n_values / 2 {
x += w - (n_values / 2) as i32;
w = (n_values / 2) as i32;
}
let mut i = n_values - (w as usize) * 2;
let mut total = 1.0f64;
if !is_percent_chart {
for j in i..n_values {
total = this.drawData.values[j].max(total);
}
}
debug_assert!(total >= 1.0);
let values = &this.drawData.values;
let mut col = 0i32;
while i < n_values - 1 {
let pix = pix_per_row * h;
let v1 = (values[i] / total * pix as f64)
.clamp(1.0, pix as f64)
.round() as i32;
let v2 = (values[i + 1] / total * pix as f64)
.clamp(1.0, pix as f64)
.round() as i32;
let mut color_idx = ColorElements::GRAPH_1;
for line in 0..h {
let line1 = (v1 - pix_per_row * (h - 1 - line)).clamp(0, pix_per_row);
let line2 = (v2 - pix_per_row * (h - 1 - line)).clamp(0, pix_per_row);
Ncurses::attrset(&mut out, color_idx.packed(scheme));
Ncurses::mvaddstr(
&mut out,
y + line,
x + col,
dots[(line1 * (pix_per_row + 1) + line2) as usize],
);
color_idx = ColorElements::GRAPH_2;
}
i += 2;
col += 1;
}
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
}
static LEDMeterMode_digitsAscii: [&str; 30] = [
" __ ", " ", " __ ", " __ ", " ", " __ ", " __ ", " __ ", " __ ", " __ ", "| |", " |",
" __|", " __|", "|__|", "|__ ", "|__ ", " |", "|__|", "|__|", "|__|", " |", "|__ ", " __|",
" |", " __|", "|__|", " |", "|__|", " __|",
];
static LEDMeterMode_digitsUtf8: [&str; 30] = [
"┌──┐",
" ┐ ",
"╶──┐",
"╶──┐",
"╷ ╷",
"┌──╴",
"┌──╴",
"╶──┐",
"┌──┐",
"┌──┐",
"│ │",
" │ ",
"┌──┘",
" ──┤",
"└──┤",
"└──┐",
"├──┐",
" │",
"├──┤",
"└──┤",
"└──┘",
" ╵ ",
"└──╴",
"╶──┘",
" ╵",
"╶──┘",
"└──┘",
" ╵",
"└──┘",
"╶──┘",
];
fn LEDMeterMode_drawDigit(mut out: &mut dyn Write, x: i32, y: i32, n: i32, digits: &[&str; 30]) {
for i in 0..3 {
Ncurses::mvaddstr(&mut out, y + i, x, digits[(i * 10 + n) as usize]);
}
}
pub fn LEDMeterMode_draw(mut out: &mut dyn Write, this: &mut Meter, x: i32, y: i32, w: i32) {
let scheme = ColorScheme::active();
let utf8 = CRT_utf8.load(Ordering::Relaxed);
let y_text = if utf8 { y + 1 } else { y + 2 };
Ncurses::attrset(&mut out, ColorElements::LED_COLOR.packed(scheme));
let caption = this.caption.clone();
if w > 0 {
Ncurses::mvaddnstr(&mut out, y_text, x, &caption, w);
}
let caption_width = if w > 0 {
(caption.len() as i32).min(w)
} else {
0
};
if w <= caption_width {
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
return;
}
let mut xx = x + caption_width;
let digits: &[&str; 30] = if utf8 {
&LEDMeterMode_digitsUtf8
} else {
&LEDMeterMode_digitsAscii
};
let mut text = RichString::new();
Meter_displayBuffer(this, &mut text);
let len = RichString_sizeVal(&text);
for i in 0..len {
let c = RichString_getCharVal(&text, i as usize);
if c.is_ascii_digit() {
if xx > x + w - 4 {
break;
}
LEDMeterMode_drawDigit(&mut out, xx, y, (c as i32) - ('0' as i32), digits);
xx += 4;
} else {
if xx > x + w - 1 {
break;
}
Ncurses::mvaddch(&mut out, y_text, xx, c);
xx += 1;
}
}
RichString_delete(&mut text);
Ncurses::attrset(&mut out, ColorElements::RESET_COLOR.packed(scheme));
}
pub fn Meter_setCaption(this: &mut Meter, caption: &str) {
this.caption = caption.to_owned();
}
pub fn Meter_setMode(this: &mut Meter, modeIndex: MeterModeId) {
if modeIndex == this.mode {
debug_assert!(this.mode > 0);
return;
}
let supportedModes = this.supportedModes;
debug_assert!(supportedModes != 0);
debug_assert!(supportedModes & (1 << 0) == 0);
const { assert!(LAST_METERMODE <= 32) };
if modeIndex >= LAST_METERMODE || (supportedModes & (1u32 << modeIndex)) == 0 {
return;
}
debug_assert!(modeIndex >= 1);
if let Some(update) = this.updateMode {
let d = this
.classDraw
.expect("Meter_drawFn must be non-null when updateMode is set");
this.draw = Some(d);
update(this, modeIndex);
} else {
this.drawData.values = Vec::new();
this.drawData.nValues = 0;
let mode = &Meter_modes[modeIndex as usize];
this.draw = mode.draw;
this.h = mode.h;
}
this.mode = modeIndex;
}
pub fn Meter_toListItem(this: &Meter, moving: bool) -> ListItem {
let mode = if this.mode > 0 {
let ui = Meter_modes[this.mode as usize]
.uiName
.expect("Meter_modes[mode].uiName is non-NULL for mode > 0");
format!(" [{ui}]")
} else {
String::new()
};
let name = if let Some(getUiName) = this.getUiName {
getUiName(this)
} else {
this.uiName.to_string()
};
let buffer = format!("{name}{mode}");
let mut li = ListItem_new(&buffer, 0);
li.moving = moving;
li
}
pub fn BlankMeter_updateValues(this: &mut Meter) {
this.txtBuffer.clear();
}
pub fn BlankMeter_display(_this: &Meter, _out: &mut RichString) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::richstring::RichString_appendAscii;
#[test]
fn zero_stays_kibibytes_no_fraction() {
assert_eq!(Meter_humanUnit(0.0), "0K");
}
#[test]
fn below_one_k_stays_kibibytes() {
assert_eq!(Meter_humanUnit(999.0), "999K");
}
#[test]
fn one_k_promotes_to_mebi_two_fraction_digits() {
assert_eq!(Meter_humanUnit(1024.0), "1.00M");
}
#[test]
fn precision_one_in_range() {
assert_eq!(Meter_humanUnit(1024.0 * 50.0), "50.0M");
}
#[test]
fn precision_zero_above_ninety_nine_nine() {
assert_eq!(Meter_humanUnit(1024.0 * 500.0), "500M");
}
#[test]
fn round_up_boundary_forces_limit() {
assert_eq!(Meter_humanUnit(1024.0 * 9.995), "10.0M");
}
#[test]
fn inf_when_still_huge_at_last_prefix() {
let v = 9999.0 * f64::powi(1024.0, 9) * 2.0;
assert_eq!(Meter_humanUnit(v), "inf");
}
#[test]
fn caps_at_last_prefix_without_inf() {
let v = 5000.0 * f64::powi(1024.0, 9);
assert_eq!(Meter_humanUnit(v), "5000Q");
}
#[test]
fn compute_sum_ignores_negatives_and_nan() {
let m = Meter {
host: core::ptr::null(),
values: vec![5.0, -3.0, f64::NAN, 2.0],
curItems: 4,
..Meter::empty()
};
assert_eq!(Meter_computeSum(&m), 7.0);
}
#[test]
fn compute_sum_honors_cur_items() {
let m = Meter {
host: core::ptr::null(),
values: vec![1.0, 2.0, 100.0],
curItems: 2,
..Meter::empty()
};
assert_eq!(Meter_computeSum(&m), 3.0);
}
#[test]
fn compute_sum_clamps_to_dbl_max() {
let m = Meter {
host: core::ptr::null(),
values: vec![f64::MAX, f64::MAX],
curItems: 2,
..Meter::empty()
};
assert_eq!(Meter_computeSum(&m), f64::MAX);
}
const ALL_MODES: u32 = METERMODE_DEFAULT_SUPPORTED;
fn mode_meter(mode: MeterModeId, supportedModes: u32) -> Meter {
Meter {
host: core::ptr::null(),
mode,
supportedModes,
..Meter::empty()
}
}
#[test]
fn next_supported_mode_cycles_through_all_modes() {
assert_eq!(
Meter_nextSupportedMode(&mode_meter(BAR_METERMODE, ALL_MODES)),
TEXT_METERMODE
);
assert_eq!(
Meter_nextSupportedMode(&mode_meter(TEXT_METERMODE, ALL_MODES)),
GRAPH_METERMODE
);
assert_eq!(
Meter_nextSupportedMode(&mode_meter(GRAPH_METERMODE, ALL_MODES)),
LED_METERMODE
);
assert_eq!(
Meter_nextSupportedMode(&mode_meter(LED_METERMODE, ALL_MODES)),
BAR_METERMODE
);
}
#[test]
fn next_supported_mode_skips_unsupported_modes() {
let supported = (1 << BAR_METERMODE) | (1 << LED_METERMODE);
assert_eq!(
Meter_nextSupportedMode(&mode_meter(BAR_METERMODE, supported)),
LED_METERMODE
);
assert_eq!(
Meter_nextSupportedMode(&mode_meter(LED_METERMODE, supported)),
BAR_METERMODE
);
}
#[test]
fn next_supported_mode_single_mode_stays_put() {
let supported = 1 << TEXT_METERMODE;
assert_eq!(
Meter_nextSupportedMode(&mode_meter(TEXT_METERMODE, supported)),
TEXT_METERMODE
);
}
#[test]
fn next_supported_mode_from_lower_than_all_supported() {
let supported = (1 << GRAPH_METERMODE) | (1 << LED_METERMODE);
assert_eq!(
Meter_nextSupportedMode(&mode_meter(BAR_METERMODE, supported)),
GRAPH_METERMODE
);
}
fn printed_chars(buf: &[u8]) -> String {
let s = String::from_utf8(buf.to_vec()).unwrap();
let mut out = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
let intro = chars.next();
if intro != Some('[') {
continue;
}
for e in chars.by_ref() {
if ('\u{40}'..='\u{7e}').contains(&e) {
break;
}
}
} else if c != '\0' {
out.push(c);
}
}
out
}
fn rich_text(r: &RichString) -> String {
(0..r.chlen as usize).map(|i| r.chptr[i].chars).collect()
}
#[test]
fn display_buffer_else_branch_writes_txt_buffer() {
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let m = Meter {
host: core::ptr::null(),
txtBuffer: "hi".to_string(),
attributes: &ATTRS,
..Meter::empty()
};
let mut out = RichString::new();
Meter_displayBuffer(&m, &mut out);
assert_eq!(rich_text(&out), "hi");
let expect =
CRT_colorSchemes[ColorScheme::active() as usize][ColorElements::METER_VALUE as usize];
assert_eq!(out.chptr[0].attr, expect & 0xffffff);
}
#[test]
fn display_buffer_dispatches_to_display_slot() {
fn disp(_this: &Meter, out: &mut RichString) {
RichString_appendAscii(out, 0, b"XY");
}
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let m = Meter {
host: core::ptr::null(),
txtBuffer: "ignored".to_string(),
attributes: &ATTRS,
display: Some(disp),
..Meter::empty()
};
let mut out = RichString::new();
Meter_displayBuffer(&m, &mut out);
assert_eq!(rich_text(&out), "XY");
}
#[test]
fn text_meter_draw_caption_then_value() {
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "50%".to_string(),
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
TextMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
assert!(printed_chars(&buf).starts_with("CPU50%"));
}
#[test]
fn text_meter_draw_zero_width_prints_nothing() {
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "50%".to_string(),
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
TextMeterMode_draw(&mut buf, &mut m, 0, 0, 0);
assert_eq!(printed_chars(&buf), "");
}
#[test]
fn text_meter_draw_caption_only_when_width_equals_caption() {
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "50%".to_string(),
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
TextMeterMode_draw(&mut buf, &mut m, 0, 0, 3);
assert_eq!(printed_chars(&buf), "CPU");
}
#[test]
fn bar_meter_draw_borders_fill_and_text() {
static ATTRS: [i32; 1] = [ColorElements::CPU_NORMAL as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "50%".to_string(),
values: vec![50.0],
curItems: 1,
total: 100.0,
isPercentChart: true,
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
BarMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
let printed = printed_chars(&buf);
assert!(printed.contains("CPU"), "printed: {printed:?}");
assert!(printed.contains('['), "printed: {printed:?}");
assert!(printed.contains(']'), "printed: {printed:?}");
assert!(printed.contains('|'), "printed: {printed:?}");
assert!(printed.contains("50%"), "printed: {printed:?}");
}
#[test]
fn bar_meter_draw_percent_chart_keeps_total() {
static ATTRS: [i32; 1] = [ColorElements::CPU_NORMAL as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "".to_string(),
values: vec![50.0],
curItems: 1,
total: 100.0,
isPercentChart: true,
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
BarMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
assert_eq!(m.total, 100.0);
}
#[test]
fn bar_meter_draw_non_percent_grows_total() {
static ATTRS: [i32; 2] = [
ColorElements::CPU_NORMAL as i32,
ColorElements::CPU_SYSTEM as i32,
];
let mut m = Meter {
host: core::ptr::null(),
caption: "IO ".to_string(),
txtBuffer: "".to_string(),
values: vec![120.0, 30.0],
curItems: 2,
total: 100.0,
isPercentChart: false,
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
BarMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
assert_eq!(m.total, 150.0);
}
#[test]
fn bar_meter_draw_curattributes_override() {
static CLASS_ATTRS: [i32; 1] = [ColorElements::CPU_NORMAL as i32];
static CUR_ATTRS: [i32; 1] = [ColorElements::CPU_SYSTEM as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "".to_string(),
values: vec![100.0],
curItems: 1,
total: 100.0,
isPercentChart: true,
attributes: &CLASS_ATTRS,
curAttributes: Some(&CUR_ATTRS),
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
BarMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
assert!(printed_chars(&buf).contains('|'));
}
#[test]
fn led_meter_draw_renders_digit_cells_and_other_chars() {
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "".to_string(),
txtBuffer: "5%".to_string(),
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
LEDMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
let printed = printed_chars(&buf);
assert!(printed.contains("__"), "printed: {printed:?}");
assert!(printed.contains('|'), "printed: {printed:?}");
assert!(printed.contains('%'), "printed: {printed:?}");
}
#[test]
fn led_meter_draw_prints_caption() {
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "".to_string(),
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
LEDMeterMode_draw(&mut buf, &mut m, 0, 0, 20);
assert!(printed_chars(&buf).contains("CPU"));
}
#[test]
fn led_meter_draw_width_equals_caption_skips_value() {
static ATTRS: [i32; 1] = [ColorElements::METER_VALUE as i32];
let mut m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
txtBuffer: "5".to_string(),
attributes: &ATTRS,
..Meter::empty()
};
let mut buf: Vec<u8> = Vec::new();
LEDMeterMode_draw(&mut buf, &mut m, 0, 0, 3);
assert_eq!(printed_chars(&buf), "CPU");
}
#[test]
fn set_mode_assigns_height_from_table() {
let mut m = Meter {
host: core::ptr::null(),
mode: BAR_METERMODE,
supportedModes: ALL_MODES,
..Meter::empty()
};
Meter_setMode(&mut m, TEXT_METERMODE);
assert_eq!(m.mode, TEXT_METERMODE);
assert_eq!(m.h, 1);
assert!(m.draw.is_some());
Meter_setMode(&mut m, GRAPH_METERMODE);
assert_eq!(m.mode, GRAPH_METERMODE);
assert_eq!(m.h, DEFAULT_GRAPH_HEIGHT);
Meter_setMode(&mut m, LED_METERMODE);
assert_eq!(m.mode, LED_METERMODE);
assert_eq!(m.h, 3);
}
#[test]
fn set_mode_resets_draw_data() {
let mut m = Meter {
host: core::ptr::null(),
mode: BAR_METERMODE,
supportedModes: ALL_MODES,
..Meter::empty()
};
m.drawData.values = vec![1.0, 2.0, 3.0];
m.drawData.nValues = 3;
Meter_setMode(&mut m, TEXT_METERMODE);
assert!(m.drawData.values.is_empty());
assert_eq!(m.drawData.nValues, 0);
}
#[test]
fn set_mode_same_mode_is_noop() {
let mut m = Meter {
host: core::ptr::null(),
mode: BAR_METERMODE,
supportedModes: ALL_MODES,
h: 42, ..Meter::empty()
};
Meter_setMode(&mut m, BAR_METERMODE);
assert_eq!(m.mode, BAR_METERMODE);
assert_eq!(m.h, 42);
}
#[test]
fn set_mode_unsupported_mode_is_rejected() {
let mut m = Meter {
host: core::ptr::null(),
mode: TEXT_METERMODE,
supportedModes: 1 << TEXT_METERMODE,
h: 7,
..Meter::empty()
};
Meter_setMode(&mut m, GRAPH_METERMODE);
assert_eq!(m.mode, TEXT_METERMODE);
assert_eq!(m.h, 7);
}
#[test]
fn set_mode_out_of_range_is_rejected() {
let mut m = Meter {
host: core::ptr::null(),
mode: BAR_METERMODE,
supportedModes: ALL_MODES,
h: 9,
..Meter::empty()
};
Meter_setMode(&mut m, LAST_METERMODE);
assert_eq!(m.mode, BAR_METERMODE);
assert_eq!(m.h, 9);
}
#[test]
fn set_mode_uses_class_update_mode_branch() {
fn upd(this: &mut Meter, mode: MeterModeId) {
this.h = 100 + mode as i32;
}
let mut m = Meter {
host: core::ptr::null(),
mode: BAR_METERMODE,
supportedModes: ALL_MODES,
updateMode: Some(upd),
classDraw: Some(BarMeterMode_draw),
..Meter::empty()
};
m.drawData.nValues = 5;
Meter_setMode(&mut m, TEXT_METERMODE);
assert_eq!(m.mode, TEXT_METERMODE);
assert_eq!(m.h, 100 + TEXT_METERMODE as i32);
assert!(m.draw.is_some());
assert_eq!(m.drawData.nValues, 5);
}
#[test]
fn blank_meter_update_values_clears_text() {
let mut m = Meter {
host: core::ptr::null(),
txtBuffer: "stale".to_string(),
..Meter::empty()
};
BlankMeter_updateValues(&mut m);
assert_eq!(m.txtBuffer, "");
}
#[test]
fn blank_meter_display_is_noop() {
let m = Meter::empty();
let mut out = RichString::new();
BlankMeter_display(&m, &mut out);
assert_eq!(out.chlen, 0);
}
#[test]
fn blank_meter_class_wires_blank_hooks() {
assert_eq!(BlankMeter_class.defaultMode, TEXT_METERMODE);
assert_eq!(BlankMeter_class.supportedModes, 1 << TEXT_METERMODE);
assert_eq!(BlankMeter_class.name, "Blank");
assert!(BlankMeter_class.updateValues.is_some());
assert!(BlankMeter_class.display.is_some());
assert_eq!(
BlankMeter_class.attributes,
&[ColorElements::DEFAULT_COLOR as i32]
);
}
#[test]
fn meter_new_builds_instance_from_class() {
use crate::ported::linux::linuxmachine::LinuxMachine;
let host = Box::leak(Box::new(LinuxMachine::default()));
let m = Meter_new(
&host.super_ as *const crate::ported::machine::Machine,
7,
&BlankMeter_class,
);
assert_eq!(m.param, 7);
assert_eq!(m.h, 1);
assert!(!m.host.is_null());
assert_eq!(m.curItems, 0);
assert!(m.values.is_empty());
assert_eq!(m.total, 0.0);
assert_eq!(m.caption, "");
assert_eq!(m.uiName, "Blank");
assert_eq!(m.supportedModes, 1 << TEXT_METERMODE);
assert!(m.updateMode.is_none());
assert!(m.display.is_some());
assert_eq!(m.mode, TEXT_METERMODE);
assert!(m.mode > 0);
}
#[test]
fn to_list_item_reserved_mode_has_no_suffix() {
let m = Meter {
host: core::ptr::null(),
uiName: "CPU",
mode: 0,
..Meter::empty()
};
let li = Meter_toListItem(&m, false);
assert_eq!(li.value, "CPU");
assert_eq!(li.key, 0);
assert!(!li.moving);
}
#[test]
fn to_list_item_real_mode_appends_mode_uiname() {
let bar = Meter {
host: core::ptr::null(),
uiName: "CPU",
mode: BAR_METERMODE,
..Meter::empty()
};
assert_eq!(Meter_toListItem(&bar, false).value, "CPU [Bar]");
let text = Meter {
host: core::ptr::null(),
uiName: "Memory",
mode: TEXT_METERMODE,
..Meter::empty()
};
assert_eq!(Meter_toListItem(&text, false).value, "Memory [Text]");
let graph = Meter {
host: core::ptr::null(),
uiName: "Swap",
mode: GRAPH_METERMODE,
..Meter::empty()
};
assert_eq!(Meter_toListItem(&graph, false).value, "Swap [Graph]");
}
#[test]
fn to_list_item_propagates_moving_flag() {
let m = Meter {
host: core::ptr::null(),
uiName: "CPU",
mode: 0,
..Meter::empty()
};
assert!(Meter_toListItem(&m, true).moving);
assert!(!Meter_toListItem(&m, false).moving);
}
#[test]
fn to_list_item_prefers_getuiname_slot_over_uiname() {
fn dyn_name(_this: &Meter) -> String {
"CPU average".to_string()
}
let m = Meter {
host: core::ptr::null(),
uiName: "STATIC-IGNORED",
getUiName: Some(dyn_name),
mode: BAR_METERMODE,
..Meter::empty()
};
assert_eq!(Meter_toListItem(&m, false).value, "CPU average [Bar]");
}
#[test]
fn to_list_item_getuiname_passes_through_full_name() {
fn long_name(_this: &Meter) -> String {
"a".repeat(40)
}
let m = Meter {
host: core::ptr::null(),
getUiName: Some(long_name),
mode: 0,
..Meter::empty()
};
assert_eq!(Meter_toListItem(&m, false).value, "a".repeat(40));
}
#[test]
fn meter_klass_is_rooted_at_object() {
let m = Meter::empty();
let k = m.klass();
assert!(core::ptr::eq(k, &Meter_class.super_));
assert!(crate::ported::object::Object_isA(
Some(&m as &dyn Object),
&Meter_class.super_
));
assert!(crate::ported::object::Object_isA(
Some(&m as &dyn Object),
&Object_class
));
}
#[test]
fn meter_display_dispatches_through_object_trait() {
let m = Meter {
host: core::ptr::null(),
display: Some(BlankMeter_display),
..Meter::empty()
};
let mut out = RichString::new();
Object::display(&m, &mut out);
assert_eq!(out.chlen, 0);
}
#[test]
fn meter_roundtrips_through_ported_vector_as_object() {
use crate::ported::vector::{Vector_add, Vector_get, Vector_new, Vector_size};
let mut v = Vector_new(&Meter_class.super_, true, 10);
let m = Meter {
host: core::ptr::null(),
caption: "CPU".to_string(),
param: 7,
..Meter::empty()
};
Vector_add(&mut v, Box::new(m));
assert_eq!(Vector_size(&v), 1);
let got: &dyn Object = Vector_get(&v, 0);
assert!(core::ptr::eq(got.klass(), &Meter_class.super_));
let any: &dyn core::any::Any = got;
let back = any
.downcast_ref::<Meter>()
.expect("stored object downcasts back to Meter");
assert_eq!(back.caption, "CPU");
assert_eq!(back.param, 7);
}
#[test]
fn meter_modes_table_matches_c() {
assert!(Meter_modes[0].draw.is_none());
assert_eq!(Meter_modes[BAR_METERMODE as usize].uiName, Some("Bar"));
assert_eq!(Meter_modes[BAR_METERMODE as usize].h, 1);
assert_eq!(Meter_modes[TEXT_METERMODE as usize].uiName, Some("Text"));
assert_eq!(Meter_modes[TEXT_METERMODE as usize].h, 1);
assert_eq!(Meter_modes[GRAPH_METERMODE as usize].uiName, Some("Graph"));
assert_eq!(
Meter_modes[GRAPH_METERMODE as usize].h,
DEFAULT_GRAPH_HEIGHT
);
assert_eq!(Meter_modes[LED_METERMODE as usize].uiName, Some("LED"));
assert_eq!(Meter_modes[LED_METERMODE as usize].h, 3);
}
}