use crate::data::ChartData;
use crate::error::Result;
use crate::layers::{Layer, LayerStage};
use crate::renderer::RenderContext;
use crate::style::ChartStyle;
use crate::theme::{ChartTheme, Color};
use crate::viewport::Viewport;
use chrono::{DateTime, Timelike, Utc};
#[derive(Debug, Clone, Copy)]
pub enum RegionBoundary {
DailySeconds(u32),
Timestamp(i64),
}
#[derive(Debug, Clone)]
pub struct ShadingRegion {
pub start: RegionBoundary,
pub end: RegionBoundary,
pub opacity: f32,
pub label: Option<String>,
}
impl ShadingRegion {
pub fn daily(start_seconds: u32, end_seconds: u32, opacity: f32) -> Self {
Self {
start: RegionBoundary::DailySeconds(start_seconds),
end: RegionBoundary::DailySeconds(end_seconds),
opacity,
label: None,
}
}
pub fn absolute(start_ts: i64, end_ts: i64, opacity: f32) -> Self {
Self {
start: RegionBoundary::Timestamp(start_ts),
end: RegionBoundary::Timestamp(end_ts),
opacity,
label: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Debug, Clone)]
pub struct RegionShadingConfig {
pub regions: Vec<ShadingRegion>,
pub default_opacity: f32,
}
impl Default for RegionShadingConfig {
fn default() -> Self {
Self {
regions: Vec::new(),
default_opacity: 0.0,
}
}
}
impl RegionShadingConfig {
pub fn with_primary_region(
start_seconds: u32,
end_seconds: u32,
primary_opacity: f32,
other_opacity: f32,
) -> Self {
Self {
regions: vec![ShadingRegion::daily(
start_seconds,
end_seconds,
primary_opacity,
)],
default_opacity: other_opacity,
}
}
}
pub struct RegionShadingLayer {
enabled: bool,
cached_geometry_hash: u64,
config: RegionShadingConfig,
base_color: Color,
}
impl RegionShadingLayer {
pub fn new() -> Self {
Self {
enabled: true,
cached_geometry_hash: 0,
config: RegionShadingConfig::default(),
base_color: Color::rgba(0, 0, 0, 0),
}
}
pub fn with_config(config: RegionShadingConfig) -> Self {
let mut layer = Self::new();
layer.config = config;
layer
}
pub fn set_regions(&mut self, regions: Vec<ShadingRegion>) {
self.config.regions = regions;
}
pub fn set_default_opacity(&mut self, opacity: f32) {
self.config.default_opacity = opacity;
}
fn recompute_colors(&mut self, theme: &ChartTheme) {
self.base_color = theme.colors.overlay;
}
fn geometry_hash(viewport: &Viewport) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = ahash::AHasher::default();
viewport.visible_time_range().hash(&mut h);
h.finish()
}
fn get_opacity_for_time(&self, secs_since_midnight: u32) -> f32 {
for region in &self.config.regions {
match (®ion.start, ®ion.end) {
(RegionBoundary::DailySeconds(start), RegionBoundary::DailySeconds(end)) => {
if *start <= *end {
if secs_since_midnight >= *start && secs_since_midnight < *end {
return region.opacity;
}
} else {
if secs_since_midnight >= *start || secs_since_midnight < *end {
return region.opacity;
}
}
}
_ => {
}
}
}
self.config.default_opacity
}
}
impl Default for RegionShadingLayer {
fn default() -> Self {
Self::new()
}
}
impl Layer for RegionShadingLayer {
fn name(&self) -> &str {
"RegionShading"
}
fn stage(&self) -> LayerStage {
LayerStage::ChartBackground
}
fn update(
&mut self,
_data: &ChartData,
viewport: &Viewport,
theme: &ChartTheme,
_style: &ChartStyle,
) {
self.recompute_colors(theme);
let hash = Self::geometry_hash(viewport);
if hash != self.cached_geometry_hash {
self.cached_geometry_hash = hash;
}
}
fn render(&self, ctx: &mut RenderContext, _render_pass: &mut wgpu::RenderPass) -> Result<()> {
if !self.enabled {
return Ok(());
}
if self.config.regions.is_empty() && self.config.default_opacity <= 0.0 {
return Ok(());
}
let viewport = ctx.viewport();
let (t_start, t_end) = viewport.visible_time_range();
let mut t = t_start;
let secs_per_day = 86_400_i64;
while t < t_end {
let dt =
DateTime::<Utc>::from_timestamp(t, 0).expect("timestamp within DateTime range");
let secs_since_midnight = dt.num_seconds_from_midnight();
let opacity = self.get_opacity_for_time(secs_since_midnight);
let color = self.base_color.with_alpha(opacity);
let mut next_boundary = secs_per_day;
for region in &self.config.regions {
if let (RegionBoundary::DailySeconds(start), RegionBoundary::DailySeconds(end)) =
(®ion.start, ®ion.end)
{
if *start > secs_since_midnight && (*start as i64) < next_boundary {
next_boundary = *start as i64;
}
if *end > secs_since_midnight && (*end as i64) < next_boundary {
next_boundary = *end as i64;
}
}
}
let day_start = dt
.date_naive()
.and_hms_opt(0, 0, 0)
.expect("valid midnight for date")
.and_utc();
let segment_end_ts = (day_start.timestamp() + next_boundary).min(t_end);
if opacity > 0.001 {
let vp = ctx.viewport();
let chart_rect = vp.chart_content_rect();
let x0 = vp.chart_to_screen_x(t as f32);
let x1 = vp.chart_to_screen_x(segment_end_ts as f32);
let width = (x1 - x0).abs();
if width > 0.5 {
let rect = crate::viewport::Rect::new(
x0.min(x1),
chart_rect.y,
width,
chart_rect.height,
);
ctx.draw_rect(rect, color);
}
}
t = segment_end_ts;
}
Ok(())
}
fn needs_render(&self) -> bool {
true
}
fn z_order(&self) -> i32 {
-5 }
fn is_enabled(&self) -> bool {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
pub type SessionShadingLayer = RegionShadingLayer;
pub type SessionShadingConfig = RegionShadingConfig;