mirror of https://github.com/terhechte/postsack
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
6.1 KiB
Rust
198 lines
6.1 KiB
Rust
use std::collections::hash_map::DefaultHasher;
|
|
|
|
use eframe::egui::{self, epaint::Galley, Color32, Pos2, Rect, Rgba, Stroke, TextStyle, Widget};
|
|
use num_format::{Locale, ToFormattedString};
|
|
use ps_core::eyre::Report;
|
|
use ps_core::model::{self, segmentations, Engine, Segment};
|
|
|
|
use super::super::platform::platform_colors;
|
|
|
|
fn segment_to_color(segment: &Segment, total: usize, position: usize) -> Color32 {
|
|
let mut hasher = DefaultHasher::new();
|
|
use std::hash::{Hash, Hasher};
|
|
let value = segment.field.value().to_string();
|
|
value.hash(&mut hasher);
|
|
let value = hasher.finish();
|
|
super::color_utils::color(value, total, position)
|
|
}
|
|
|
|
pub struct Rectangles<'a> {
|
|
engine: &'a mut Engine,
|
|
error: &'a mut Option<Report>,
|
|
}
|
|
|
|
impl<'a> Rectangles<'a> {
|
|
pub fn new(engine: &'a mut Engine, error: &'a mut Option<Report>) -> Self {
|
|
Rectangles { engine, error }
|
|
}
|
|
}
|
|
|
|
impl<'a> Widget for Rectangles<'a> {
|
|
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
|
|
let size = ui.available_size();
|
|
let (rect, mut response) = ui.allocate_exact_size(size, egui::Sense::hover());
|
|
|
|
let items = match segmentations::layouted_segments(self.engine, convert_rect(rect)) {
|
|
Some(n) => n.to_owned(),
|
|
None => return response,
|
|
};
|
|
|
|
let active = segmentations::can_aggregate_more(self.engine);
|
|
|
|
let colors = platform_colors();
|
|
|
|
let total = items.len();
|
|
let mut hovered: Option<String> = None;
|
|
for (index, item) in items.iter().enumerate() {
|
|
let item_response = ui.put(
|
|
convert_rect_back(item.layout_rect()),
|
|
rectangle(item, active, colors.content_background, index, total),
|
|
);
|
|
if item_response.clicked() && active {
|
|
*self.error = self.engine.push(item.clone()).err();
|
|
response.mark_changed();
|
|
}
|
|
if item_response.hovered() {
|
|
hovered = Some(format!("{}: #{}", item.field.to_string(), item.count));
|
|
}
|
|
}
|
|
|
|
if let Some(text) = hovered {
|
|
// Calculate the size
|
|
let galley = ui
|
|
.painter()
|
|
.layout_no_wrap(text.clone(), TextStyle::Body, Color32::WHITE);
|
|
|
|
// keep spacing in mind
|
|
let size = galley.size();
|
|
let size: Pos2 = (
|
|
size.x + ui.spacing().button_padding.x * 2.0,
|
|
size.y + ui.spacing().button_padding.y * 2.0,
|
|
)
|
|
.into();
|
|
|
|
// we build a disabled for easy rounded corners
|
|
|
|
let label = egui::widgets::Label::new(text)
|
|
.background_color(colors.window_background)
|
|
.text_color(colors.text_primary);
|
|
|
|
// we want to be a wee bit in the rectangle system
|
|
let offset = -2.0;
|
|
ui.put(
|
|
Rect::from_min_size(
|
|
(
|
|
rect.left() - offset,
|
|
(rect.bottom() + offset) - (size.y + 10.0),
|
|
)
|
|
.into(),
|
|
(size.x + 10.0, size.y + 10.0).into(),
|
|
),
|
|
label,
|
|
);
|
|
}
|
|
|
|
response
|
|
}
|
|
}
|
|
|
|
fn rectangle_ui(
|
|
ui: &mut egui::Ui,
|
|
segment: &Segment,
|
|
active: bool,
|
|
stroke_color: Color32,
|
|
position: usize,
|
|
total: usize,
|
|
) -> egui::Response {
|
|
let size = ui.available_size();
|
|
let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click());
|
|
|
|
//let visuals = ui.style().interact_selectable(&response, true);
|
|
|
|
let stroke = Stroke::new(1.0, stroke_color);
|
|
|
|
let color = segment_to_color(segment, total, position);
|
|
let color = if ui.ui_contains_pointer() && active {
|
|
Color32::from_rgb(
|
|
color.r().saturating_add(25),
|
|
color.g().saturating_add(25),
|
|
color.b().saturating_add(25),
|
|
)
|
|
} else {
|
|
color
|
|
};
|
|
|
|
let painter = ui.painter();
|
|
|
|
painter.rect(rect, 2.0, color, stroke);
|
|
let mut center = rect.center();
|
|
|
|
let align_bottom = |galley: &std::sync::Arc<Galley>, center: &mut Pos2, spacing: f32| {
|
|
#[allow(clippy::clone_on_copy)]
|
|
let mut position = center.clone();
|
|
let size = galley.size();
|
|
position.x -= size.x / 2.0;
|
|
position.y -= size.y / 2.0;
|
|
center.y += size.y + spacing;
|
|
if size.x < rect.width() && size.y < rect.height() {
|
|
Some(position)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
// Write the label and the amount
|
|
{
|
|
// Take the max width - some spacing to fit into the rectangle
|
|
let width = rect.width() - ui.spacing().button_padding.x * 2.0;
|
|
let text = segment.field.to_string();
|
|
let galley = painter.layout(text, TextStyle::Body, Rgba::BLACK.into(), width);
|
|
let previous_center = center;
|
|
if let Some(center) = align_bottom(&galley, &mut center, 2.0) {
|
|
painter.galley(center, galley);
|
|
} else {
|
|
// If the name doesn't fit, reverse the changes to center the count
|
|
center = previous_center;
|
|
}
|
|
}
|
|
{
|
|
let text = segment.count.to_formatted_string(&Locale::en);
|
|
let galley = painter.layout_no_wrap(text, TextStyle::Small, Rgba::BLACK.into());
|
|
if let Some(center) = align_bottom(&galley, &mut center, 5.0) {
|
|
painter.galley(center, galley);
|
|
}
|
|
}
|
|
response
|
|
}
|
|
|
|
fn rectangle(
|
|
segment: &Segment,
|
|
active: bool,
|
|
stroke_color: Color32,
|
|
position: usize,
|
|
total: usize,
|
|
) -> impl egui::Widget + '_ {
|
|
move |ui: &mut egui::Ui| rectangle_ui(ui, segment, active, stroke_color, position, total)
|
|
}
|
|
|
|
// Can't implement into / from as the trait is in another
|
|
// crate. Instead of a newtype, to simple fns
|
|
fn convert_rect(rect: Rect) -> model::Rect {
|
|
model::Rect {
|
|
left: rect.left() as f64,
|
|
top: rect.top() as f64,
|
|
width: rect.width() as f64,
|
|
height: rect.height() as f64,
|
|
}
|
|
}
|
|
|
|
fn convert_rect_back(rect: model::Rect) -> Rect {
|
|
Rect::from_min_size(
|
|
Pos2::new(rect.left as f32, rect.top as f32),
|
|
egui::Vec2 {
|
|
x: rect.width as f32,
|
|
y: rect.height as f32,
|
|
},
|
|
)
|
|
}
|