use crate::HeaderSortState;
use egui::{
Color32, Context,
FontFamily::Proportional,
FontId, Frame, Response, RichText, Sense, Spacing, Stroke, Style,
TextStyle::{self, Body, Button, Heading, Monospace, Small},
Ui, Vec2, Visuals, Window,
style::ScrollStyle,
};
use polars::prelude::*;
use std::{collections::HashSet, ffi::OsStr, hash::Hash, path::Path};
pub const CUSTOM_TEXT_STYLE: [(egui::TextStyle, egui::FontId); 5] = [
(Heading, FontId::new(18.0, Proportional)),
(Body, FontId::new(16.0, Proportional)),
(Button, FontId::new(16.0, Proportional)),
(Monospace, FontId::new(16.0, Proportional)), (Small, FontId::new(14.0, Proportional)),
];
pub trait MyStyle {
fn set_style_init(&self, visuals: Visuals);
}
impl MyStyle for Context {
fn set_style_init(&self, visuals: Visuals) {
let scroll = ScrollStyle {
handle_min_length: 32.0,
..ScrollStyle::default()
};
let spacing = Spacing {
scroll,
item_spacing: [8.0, 6.0].into(),
..Spacing::default()
};
let style = Style {
visuals, spacing, text_styles: CUSTOM_TEXT_STYLE.into(), ..Style::default()
};
self.set_global_style(style);
}
}
pub trait Notification: Send + Sync + 'static {
fn show(&mut self, ctx: &Context) -> bool;
}
pub struct Settings {}
impl Notification for Settings {
fn show(&mut self, ctx: &Context) -> bool {
let mut open = true;
Window::new("Settings")
.collapsible(false) .open(&mut open)
.show(ctx, |ui| {
ctx.style_ui(ui, egui::Theme::Dark);
ui.disable(); });
open }
}
pub struct Error {
pub message: String,
}
impl Notification for Error {
fn show(&mut self, ctx: &Context) -> bool {
let mut open = true; let width_min = 500.0;
Window::new("Error")
.collapsible(false) .resizable(true) .min_width(width_min)
.open(&mut open)
.show(ctx, |ui| {
Frame::default()
.fill(Color32::from_rgb(255, 200, 200)) .stroke(Stroke::new(1.0, Color32::DARK_RED)) .inner_margin(10.0) .show(ui, |ui| {
ui.set_max_width(ui.available_width()); ui.colored_label(Color32::BLACK, &self.message);
});
});
open }
}
pub trait SortableHeaderRenderer {
fn render_sortable_header(
&mut self,
column_name: &str,
interaction_state: &HeaderSortState, sort_index: Option<usize>, use_enhanced_style: bool,
) -> Response;
}
impl SortableHeaderRenderer for Ui {
fn render_sortable_header(
&mut self,
column_name: &str,
interaction_state: &HeaderSortState, sort_index: Option<usize>, use_enhanced_style: bool,
) -> Response {
let column_name_color = get_column_header_text_color(self.visuals());
let icon_string = interaction_state.get_icon(sort_index);
let text_style = TextStyle::Button;
let max_potential_icon_str = "99⇧"; let icon_container_size =
calculate_icon_container_size_for_string(self, &text_style, max_potential_icon_str);
let outer_response = self.horizontal_centered(|ui| {
ui.style_mut().override_text_style = Some(text_style.clone());
let msg1 = format!("Click to sort by: {column_name:#?}");
let msg2 = "↕ Not Sorted";
let msg3 = "Sort with Nulls First:";
let msg4 = " ⏷ Sort in Descending order";
let msg5 = " ⏶ Sort in Ascending order";
let msg6 = "Sort with Nulls Last:";
let msg7 = " ⬇ Sort in Descending order";
let msg8 = " ⬆ Sort in Ascending order";
let msg = [&msg1, "", msg2, msg3, msg4, msg5, msg6, msg7, msg8].join("\n");
let icon_response = ui
.add_sized(icon_container_size, |ui: &mut Ui| {
ui.centered_and_justified(|ui| {
ui.add(egui::Label::new(&icon_string).sense(Sense::click()))
})
.inner })
.on_hover_text(msg);
ui.add(if use_enhanced_style {
egui::Label::new(RichText::new(column_name).color(column_name_color)).wrap()
} else {
egui::Label::new(RichText::new(column_name))
});
icon_response
});
outer_response.inner
}
}
fn get_column_header_text_color(visuals: &Visuals) -> Color32 {
if visuals.dark_mode {
Color32::from_rgb(160, 200, 255) } else {
Color32::from_rgb(0, 80, 160) }
}
fn calculate_icon_container_size_for_string(
ui: &Ui,
text_style: &TextStyle,
sample_str: &str,
) -> Vec2 {
let text_height = ui.text_style_height(text_style);
let max_width = {
let font_id = text_style.resolve(ui.style());
let galley = ui
.fonts_mut(|f| f.layout_no_wrap(sample_str.to_string(), font_id, Color32::PLACEHOLDER));
(galley.size().x + 2.0).ceil() };
Vec2::new(max_width, text_height)
}
pub trait PathExtension {
fn extension_as_lowercase(&self) -> Option<String>;
}
impl PathExtension for Path {
fn extension_as_lowercase(&self) -> Option<String> {
self.extension() .and_then(OsStr::to_str) .map(str::to_lowercase) }
}
pub trait UniqueElements<T> {
fn unique(&mut self)
where
T: Eq + Hash + Clone;
}
impl<T> UniqueElements<T> for Vec<T> {
fn unique(&mut self)
where
T: Eq + Hash + Clone, {
let mut seen = HashSet::new(); self.retain(|x| {
seen.insert(x.clone()) });
}
}
pub trait LazyFrameExtension {
fn round_float_columns(self, decimals: u32) -> Self;
}
impl LazyFrameExtension for LazyFrame {
fn round_float_columns(self, decimals: u32) -> Self {
let float_cols_selector = dtype_cols(&[DataType::Float32, DataType::Float64])
.as_selector()
.as_expr();
self.with_columns([
float_cols_selector
.round(decimals, RoundMode::HalfAwayFromZero)
.name()
.keep(), ])
}
}
#[cfg(test)]
mod tests_path_extension {
use super::*;
use std::path::PathBuf;
#[test]
fn test_extension_as_lowercase_some() {
let path = PathBuf::from("my_file.TXT");
assert_eq!(path.extension_as_lowercase(), Some("txt".to_string()));
}
#[test]
fn test_extension_as_lowercase_none() {
let path = PathBuf::from("myfile");
assert_eq!(path.extension_as_lowercase(), None);
}
#[test]
fn test_extension_as_lowercase_no_final_part() {
let path = PathBuf::from("path/to/directory/."); assert_eq!(path.extension_as_lowercase(), None);
}
#[test]
fn test_extension_as_lowercase_multiple_dots() {
let path = PathBuf::from("file.name.with.multiple.dots.ext");
assert_eq!(path.extension_as_lowercase(), Some("ext".to_string()));
}
}
#[cfg(test)]
mod tests_unique {
use super::*;
#[test]
fn test_unique() {
let mut vec = vec![1, 2, 2, 3, 1, 4, 3, 2, 5];
vec.unique();
assert_eq!(vec, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_unique_empty() {
let mut vec: Vec<i32> = vec![];
vec.unique();
assert_eq!(vec, Vec::<i32>::new());
}
#[test]
fn test_unique_all_same() {
let mut vec = vec![1, 1, 1, 1, 1];
vec.unique();
assert_eq!(vec, vec![1]);
}
#[test]
fn test_unique_strings() {
let mut vec = vec!["a", "b", "b", "c", "a", "d", "c", "b", "e"];
vec.unique();
assert_eq!(vec, vec!["a", "b", "c", "d", "e"]);
}
}
#[cfg(test)]
mod tests_format_columns {
use super::*;
#[test]
fn round_float_columns() -> PolarsResult<()> {
let df_input = df!(
"int_col" => &[Some(1), Some(2), None],
"f32_col" => &[Some(1.2345f32), None, Some(3.9876f32)],
"f64_col" => &[None, Some(10.11111), Some(-5.55555)],
"str_col" => &[Some("a"), Some("b"), Some("c")],
"float_col" => &[1.1234, 2.5650001, 3.965000],
"opt_float" => &[Some(1.0), None, Some(3.45677)],
)?;
let df_expected = df!(
"int_col" => &[Some(1), Some(2), None],
"f32_col" => &[Some(1.23f32), None, Some(3.99f32)],
"f64_col" => &[None, Some(10.11), Some(-5.56)],
"str_col" => &[Some("a"), Some("b"), Some("c")],
"float_col" => &[1.12, 2.57, 3.97],
"opt_float" => &[Some(1.0), None, Some(3.46)],
)?;
let decimals = 2;
dbg!(&df_input);
dbg!(&decimals);
let df_output = df_input.lazy().round_float_columns(decimals).collect()?;
dbg!(&df_output);
assert!(
df_output.equals_missing(&df_expected),
"Failed round float columns.\nOutput:\n{df_output:?}\nExpected:\n{df_expected:?}"
);
Ok(())
}
#[test]
fn round_no_float_columns() -> PolarsResult<()> {
let df_input = df!(
"int_col" => &[1, 2, 3],
"str_col" => &["x", "y", "z"]
)?;
let df_expected = df_input.clone();
let decimals = 2;
dbg!(&df_input);
dbg!(&decimals);
let df_output = df_input.lazy().round_float_columns(decimals).collect()?;
dbg!(&df_output);
assert!(df_output.equals(&df_expected)); Ok(())
}
#[test]
fn round_with_zero_decimals() -> PolarsResult<()> {
let df_input = df!(
"f64_col" => &[1.2, 1.8, -0.4, -0.9]
)?;
let df_expected = df!(
"f64_col" => &[1.0, 2.0, 0.0, -1.0] )?;
let decimals = 0;
dbg!(&df_input);
dbg!(&decimals);
let df_output = df_input.lazy().round_float_columns(decimals).collect()?;
dbg!(&df_output);
assert!(df_output.equals_missing(&df_expected));
Ok(())
}
}