diff --git a/Cargo.lock b/Cargo.lock index 2d79ef24..80cc08d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1698,9 +1698,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmath" diff --git a/ratatui-core/src/layout/layout.rs b/ratatui-core/src/layout/layout.rs index 49f5bc54..6d6a677f 100644 --- a/ratatui-core/src/layout/layout.rs +++ b/ratatui-core/src/layout/layout.rs @@ -985,6 +985,20 @@ fn configure_fill_constraints( Ok(()) } +// Used instead of `f64::round` directly, to provide fallback for `no_std`. +#[cfg(feature = "std")] +#[inline] +fn round(value: f64) -> f64 { + value.round() +} + +// A rounding fallback for `no_std` in pure rust. +#[cfg(not(feature = "std"))] +#[inline] +fn round(value: f64) -> f64 { + (value + 0.5f64.copysign(value)) as i64 as f64 +} + fn changes_to_rects( changes: &HashMap, elements: &[Element], @@ -997,8 +1011,8 @@ fn changes_to_rects( .map(|element| { let start = changes.get(&element.start).unwrap_or(&0.0); let end = changes.get(&element.end).unwrap_or(&0.0); - let start = (start.round() / FLOAT_PRECISION_MULTIPLIER).round() as u16; - let end = (end.round() / FLOAT_PRECISION_MULTIPLIER).round() as u16; + let start = round(round(*start) / FLOAT_PRECISION_MULTIPLIER) as u16; + let end = round(round(*end) / FLOAT_PRECISION_MULTIPLIER) as u16; let size = end.saturating_sub(start); match direction { Direction::Horizontal => Rect { diff --git a/ratatui-widgets/Cargo.toml b/ratatui-widgets/Cargo.toml index 356e3986..519515d5 100644 --- a/ratatui-widgets/Cargo.toml +++ b/ratatui-widgets/Cargo.toml @@ -24,6 +24,9 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["all-widgets"] +## enables std +std = [] + ## enables serialization and deserialization of style and color types using the [`serde`] crate. ## This is useful if you want to save themes to a file. serde = ["dep:serde", "ratatui-core/serde"] diff --git a/ratatui-widgets/src/canvas.rs b/ratatui-widgets/src/canvas.rs index f1d4671a..618073fd 100644 --- a/ratatui-widgets/src/canvas.rs +++ b/ratatui-widgets/src/canvas.rs @@ -34,6 +34,8 @@ pub use self::map::{Map, MapResolution}; pub use self::points::Points; pub use self::rectangle::Rectangle; use crate::block::{Block, BlockExt}; +#[cfg(not(feature = "std"))] +use crate::polyfills::F64Polyfills; mod circle; mod line; diff --git a/ratatui-widgets/src/canvas/circle.rs b/ratatui-widgets/src/canvas/circle.rs index c53e09e0..1c39de11 100644 --- a/ratatui-widgets/src/canvas/circle.rs +++ b/ratatui-widgets/src/canvas/circle.rs @@ -1,6 +1,8 @@ use ratatui_core::style::Color; use crate::canvas::{Painter, Shape}; +#[cfg(not(feature = "std"))] +use crate::polyfills::F64Polyfills; /// A circle with a given center and radius and with a given color #[derive(Debug, Default, Clone, PartialEq)] diff --git a/ratatui-widgets/src/gauge.rs b/ratatui-widgets/src/gauge.rs index 5dd0a8d1..19c79a25 100644 --- a/ratatui-widgets/src/gauge.rs +++ b/ratatui-widgets/src/gauge.rs @@ -9,6 +9,8 @@ use ratatui_core::text::{Line, Span}; use ratatui_core::widgets::Widget; use crate::block::{Block, BlockExt}; +#[cfg(not(feature = "std"))] +use crate::polyfills::F64Polyfills; /// A widget to display a progress bar. /// diff --git a/ratatui-widgets/src/lib.rs b/ratatui-widgets/src/lib.rs index d38bc27c..711b9bf5 100644 --- a/ratatui-widgets/src/lib.rs +++ b/ratatui-widgets/src/lib.rs @@ -86,6 +86,8 @@ #![warn(clippy::alloc_instead_of_core)] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; pub mod barchart; pub mod block; @@ -103,6 +105,8 @@ pub mod sparkline; pub mod table; pub mod tabs; +#[cfg(not(feature = "std"))] +mod polyfills; mod reflow; #[cfg(feature = "calendar")] diff --git a/ratatui-widgets/src/polyfills.rs b/ratatui-widgets/src/polyfills.rs new file mode 100644 index 00000000..621569e0 --- /dev/null +++ b/ratatui-widgets/src/polyfills.rs @@ -0,0 +1,156 @@ +//! Polyfills providing pure rust fallback implementations of various `f64` methods for `no_std` +//! compatibility. These implementations may not be as accurate as their built-in counterparts. +//! This must be taken into account when using floating point math in this crate. +//! +//! Implementations based on [`micromath`](https://github.com/tarcieri/micromath) crate. +//! +//! Related Rust tracking issues: +//! +//! - +//! - +use core::f64::consts::{FRAC_1_PI, PI}; + +#[inline] +fn mul_add(val: f64, a: f64, b: f64) -> f64 { + val * a + b +} + +#[inline] +fn round(val: f64) -> f64 { + (val + 0.5f64.copysign(val)) as i64 as f64 +} + +#[inline] +fn floor(val: f64) -> f64 { + let mut res = (val as i64) as f64; + if val < res { + res -= 1.0; + } + res +} + +#[inline] +fn sin(val: f64) -> f64 { + cos(val - PI / 2.0) +} + +#[inline] +fn cos(val: f64) -> f64 { + let mut x = val; + x *= FRAC_1_PI / 2.0; + x -= 0.25 + floor(x + 0.25); + x *= 16.0 * (x.abs() - 0.5); + x += 0.225 * x * (x.abs() - 1.0); + x +} + +pub(crate) trait F64Polyfills { + /// Computes `(self * a) + b`. + fn mul_add(self, a: f64, b: f64) -> f64; + + /// Returns the nearest integer to `self`. If a value is half-way between two integers, round + /// away from `0.0`. + fn round(self) -> f64; + + /// Returns the largest integer less than or equal to `self`. + fn floor(self) -> f64; + + /// Approximates the sine of a number (in radians) with max error of `0.002`. + fn sin(self) -> f64; + + /// Approximates the cosine of a number (in radians) with max error of `0.002`. + fn cos(self) -> f64; +} + +impl F64Polyfills for f64 { + #[inline] + fn mul_add(self, a: f64, b: f64) -> f64 { + mul_add(self, a, b) + } + #[inline] + fn round(self) -> f64 { + round(self) + } + #[inline] + fn floor(self) -> f64 { + floor(self) + } + #[inline] + fn sin(self) -> f64 { + sin(self) + } + #[inline] + fn cos(self) -> f64 { + cos(self) + } +} + +#[cfg(test)] +mod tests { + use core::f64::consts::{FRAC_PI_2, PI, TAU}; + + use super::*; + // explicitly use std to prevent testing against itself + extern crate std; + + const TEST_VALUES: [f64; 24] = [ + 0.0, 0.5, -0.5, 1.0, -1.0, PI, -PI, FRAC_PI_2, -FRAC_PI_2, TAU, -TAU, 9.4248, -9.4248, + 0.2528, -7.7047, -1.1596, -2.6095, 6.8435, 3.5392, 5.9725, 0.9172, 9.3539, 2.8843, -1.8483, + ]; + + const MAX_ERROR: f64 = 0.000_000_000_000_01; + const TRIG_MAX_ERROR: f64 = 0.002; + + fn assert_with_error(computed: f64, expected: f64, max_error: f64) { + let delta = (computed - expected).abs(); + assert!( + delta <= max_error, + "error exceeded max value of {max_error}: {computed} vs {expected}" + ); + } + + #[test] + fn f64_mul_add() { + for chunk in TEST_VALUES.chunks(3) { + let expected = chunk[0].mul_add(chunk[1], chunk[2]); + let computed = mul_add(chunk[0], chunk[1], chunk[2]); + assert_with_error(computed, expected, MAX_ERROR); + } + } + + #[test] + fn f64_round() { + for value in TEST_VALUES { + let expected = value.round(); + let computed = round(value); + assert_with_error(computed, expected, MAX_ERROR); + } + } + + #[test] + fn f64_floor() { + for value in TEST_VALUES { + let expected = value.floor(); + let computed = floor(value); + assert_with_error(computed, expected, MAX_ERROR); + } + } + + #[test] + fn f64_sin() { + for value in TEST_VALUES { + let expected = value.sin(); + let computed = sin(value); + assert_with_error(computed, expected, TRIG_MAX_ERROR); + } + } + + #[test] + fn f64_cos() { + for value in TEST_VALUES { + let expected = value.cos(); + let computed = cos(value); + assert_with_error(computed, expected, TRIG_MAX_ERROR); + } + } +} diff --git a/ratatui-widgets/src/scrollbar.rs b/ratatui-widgets/src/scrollbar.rs index 27dfd242..21e65349 100644 --- a/ratatui-widgets/src/scrollbar.rs +++ b/ratatui-widgets/src/scrollbar.rs @@ -17,6 +17,9 @@ use ratatui_core::widgets::StatefulWidget; use strum::{Display, EnumString}; use unicode_width::UnicodeWidthStr; +#[cfg(not(feature = "std"))] +use crate::polyfills::F64Polyfills; + /// A widget to display a scrollbar /// /// The following components of the scrollbar are customizable in symbol and style. Note the diff --git a/ratatui/Cargo.toml b/ratatui/Cargo.toml index ac85c167..52d5b1f3 100644 --- a/ratatui/Cargo.toml +++ b/ratatui/Cargo.toml @@ -38,7 +38,7 @@ termion = ["std", "dep:ratatui-termion"] termwiz = ["std", "dep:ratatui-termwiz"] ## enables std -std = ["ratatui-core/std"] +std = ["ratatui-core/std", "ratatui-widgets/std"] #! The following optional features are available for all backends: ## enables serialization and deserialization of style and color types using the [`serde`] crate.