mod action_row;
mod banner;
mod chip;
mod cluster_bloom;
mod cluster_naming;
mod cluster_overflow;
mod exit_confirm;
mod focus_cycle;
mod hover_label;
mod screenshot;
mod selection_marker;
mod state;
mod style;
mod text;
mod toast;
mod view;
use std::error::Error;
use smithay::{
backend::renderer::gles::GlesFrame,
utils::{Physical, Rectangle},
};
use crate::compositor::root::Halley;
pub(crate) use cluster_bloom::{
bloom_token_hit_test, draw_cluster_bloom, ensure_cluster_bloom_icon_resources,
};
pub(crate) use cluster_naming::{
ClusterNamingDialogHit, cluster_naming_dialog_hit_test, draw_cluster_naming_dialog,
};
pub(crate) use cluster_overflow::{
cluster_overflow_icon_hit_test, cluster_overflow_strip_slot_at,
draw_cluster_overflow_promotion, draw_cluster_overflow_strip,
};
pub(crate) use hover_label::draw_overlay_hover_label;
pub(crate) use screenshot::{ScreenshotMenuHit, draw_screenshot_overlay, screenshot_menu_hit_test};
pub(crate) use selection_marker::draw_cluster_selection_markers;
pub(crate) use state::{
ClusterBloomAnimSnapshot, ClusterBloomAnimState, ExitConfirmOverlaySnapshot,
ExitConfirmOverlayState, OverlayActionHint, OverlayBannerSnapshot, OverlayBannerState,
OverlayToastKind, OverlayToastSnapshot, OverlayToastState,
};
#[cfg(test)]
use style::color_luminance;
use style::{OverlayVisuals, overlay_accent_fill, overlay_text_mix, resolve_overlay_visuals};
pub(crate) use style::{overlay_fill_and_text_colors, overlay_text_color_for_fill};
pub(crate) use toast::{error_toast_hit_test, scroll_error_toast};
pub(crate) use view::OverlayView;
use action_row::{draw_overlay_action_row, overlay_action_row_size};
use banner::draw_persistent_banner;
use chip::{
draw_overlay_chip, draw_overlay_chip_with_border_color, draw_overlay_chip_without_shadow,
};
use cluster_overflow::draw_overflow_member_chip;
use exit_confirm::draw_exit_confirmation;
use focus_cycle::draw_focus_cycle_switcher;
use text::{truncate_overlay_text, truncate_overlay_text_to_width, visible_overlay_text_window};
use toast::draw_toast;
const BANNER_PAD_X: i32 = 14;
const BANNER_PAD_Y: i32 = 10;
const BANNER_GAP: i32 = 6;
const BANNER_EDGE_PAD: i32 = 18;
const BANNER_TITLE_SCALE: i32 = 2;
const BANNER_META_SCALE: i32 = 2;
const ACTION_ROW_GAP_Y: i32 = 10;
const ACTION_ITEM_GAP: i32 = 18;
const ACTION_LABEL_GAP: i32 = 8;
const ACTION_KEY_PAD_X: i32 = 8;
const ACTION_KEY_PAD_Y: i32 = 6;
const ACTION_KEY_MIN_W: i32 = 48;
const ACTION_KEY_SCALE: i32 = BANNER_META_SCALE;
const ACTION_LABEL_SCALE: i32 = BANNER_META_SCALE;
const SELECT_MARKER_SCALE: i32 = 2;
const TOAST_PAD_X: i32 = 14;
const TOAST_PAD_Y: i32 = 10;
const TOAST_SCALE: i32 = 2;
const TOAST_META_SCALE: i32 = 2;
const ERROR_TOAST_BODY_PAD_X: i32 = 8;
const ERROR_TOAST_BODY_PAD_Y: i32 = 6;
const ERROR_TOAST_BODY_MAX_H: i32 = 120;
const ERROR_TOAST_LINE_GAP: i32 = 5;
const ERROR_TOAST_SCROLLBAR_W: i32 = 4;
const FOCUS_CYCLE_BACKDROP_ALPHA: f32 = 0.20;
const FOCUS_CYCLE_GAP: i32 = 18;
const FOCUS_CYCLE_ICON_PAD: i32 = 10;
const FOCUS_CYCLE_CARD_PAD_X: i32 = 14;
const FOCUS_CYCLE_LABEL_SCALE: i32 = 2;
const FOCUS_CYCLE_META_SCALE: i32 = 1;
const FOCUS_CYCLE_MONITOR_SCALE: i32 = 1;
const FOCUS_CYCLE_VISIBLE_RADIUS: i32 = 3;
const EXIT_CONFIRM_PAD_X: i32 = 18;
const EXIT_CONFIRM_PAD_Y: i32 = 16;
const EXIT_CONFIRM_TITLE_SCALE: i32 = 2;
const EXIT_CONFIRM_MIN_WIDTH: i32 = 280;
const EXIT_CONFIRM_MAX_WIDTH_PAD: i32 = 36;
const SELECT_MARKER_PAD_X: i32 = 8;
const SELECT_MARKER_PAD_Y: i32 = 4;
const OVERFLOW_ICON_PAD: i32 = 8;
const OVERFLOW_ICON_SIZE: i32 = 40;
const OVERFLOW_ICON_GAP: i32 = 8;
const OVERFLOW_VISIBLE_SLOTS: usize = 15;
const OVERFLOW_SCROLLBAR_W: i32 = 4;
const OVERFLOW_SCROLLBAR_PAD: i32 = 6;
const OVERFLOW_REVEAL_ANIM_MS: u64 = 220;
const OVERFLOW_REVEAL_SLIDE_PX: i32 = 28;
const EXIT_CONFIRM_TITLE: &str = "Are you sure you want to leave?";
pub(crate) fn draw_monitor_hud(
frame: &mut GlesFrame<'_, '_>,
st: &mut Halley,
screen_w: i32,
screen_h: i32,
damage: Rectangle<i32, Physical>,
now: std::time::Instant,
) -> Result<(), Box<dyn Error>> {
let overlay_monitor = st.model.monitor_state.current_monitor.clone();
let visuals = resolve_overlay_visuals(&st.runtime.tuning);
if let Some(exit_confirm) = st
.ui
.render_state
.exit_confirm_snapshot(overlay_monitor.as_str())
{
draw_exit_confirmation(
frame,
&st.ui.render_state,
&visuals,
&st.runtime.tuning.font,
screen_w,
screen_h,
damage,
&exit_confirm,
)?;
return Ok(());
}
if draw_focus_cycle_switcher(frame, st, screen_w, screen_h, damage)? {
return Ok(());
}
if let Some(banner) = st
.ui
.render_state
.persistent_mode_banner_snapshot(overlay_monitor.as_str())
{
draw_persistent_banner(
frame,
&st.ui.render_state,
&visuals,
&st.runtime.tuning.font,
damage,
&banner,
)?;
}
if let Some(toast) = st
.ui
.render_state
.overlay_toast_snapshot(overlay_monitor.as_str(), st.now_ms(now))
{
draw_toast(
frame,
&st.ui.render_state,
&visuals,
&st.runtime.tuning.font,
screen_w,
screen_h,
damage,
&toast,
)?;
}
draw_cluster_naming_dialog(frame, st, screen_w, screen_h, damage)?;
draw_screenshot_overlay(frame, st, screen_w, screen_h, damage)?;
Ok(())
}
#[cfg(test)]
mod tests {
use halley_config::{
DecorationBorderColor, OverlayBorderSource, OverlayColorMode, OverlayShape,
};
use super::{overlay_accent_fill, overlay_text_color_for_fill, resolve_overlay_visuals};
#[test]
fn overlay_auto_text_tracks_background_contrast() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.overlay_style.background_color = OverlayColorMode::Dark;
let visuals = resolve_overlay_visuals(&tuning);
assert!(visuals.palette.text.luminance() > visuals.palette.fill.luminance());
}
#[test]
fn overlay_shape_and_border_width_follow_overlay_config() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.decorations.border.size_px = 5;
tuning.overlay_style.shape = OverlayShape::Rounded;
tuning.overlay_style.borders = true;
let visuals = resolve_overlay_visuals(&tuning);
assert!(visuals.rounded);
assert_eq!(visuals.border_px, 5.0);
tuning.overlay_style.borders = false;
let visuals = resolve_overlay_visuals(&tuning);
assert_eq!(visuals.border_px, 0.0);
}
#[test]
fn overlay_secondary_border_source_uses_secondary_style_when_enabled() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.decorations.secondary_border.enabled = true;
tuning.decorations.secondary_border.size_px = 2;
tuning.decorations.secondary_border.color_focused = DecorationBorderColor {
r: 0.9,
g: 0.8,
b: 0.1,
};
tuning.overlay_style.border_source = OverlayBorderSource::Secondary;
let visuals = resolve_overlay_visuals(&tuning);
assert_eq!(visuals.border_px, 2.0);
assert_eq!(
(
visuals.palette.border.r,
visuals.palette.border.g,
visuals.palette.border.b
),
(0.9, 0.8, 0.1)
);
}
#[test]
fn overlay_secondary_border_source_falls_back_to_primary_when_disabled() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.decorations.border.size_px = 4;
tuning.decorations.border.color_focused = DecorationBorderColor {
r: 0.1,
g: 0.2,
b: 0.3,
};
tuning.overlay_style.border_source = OverlayBorderSource::Secondary;
let visuals = resolve_overlay_visuals(&tuning);
assert_eq!(visuals.border_px, 4.0);
assert_eq!(
(
visuals.palette.border.r,
visuals.palette.border.g,
visuals.palette.border.b
),
(0.1, 0.2, 0.3)
);
}
#[test]
fn overlay_accent_fill_pulls_toward_border_color() {
let tuning = halley_config::RuntimeTuning::default();
let visuals = resolve_overlay_visuals(&tuning);
let accent = overlay_accent_fill(&visuals, 0.5, 1.0);
assert_ne!(accent.r(), visuals.palette.fill.r);
assert_ne!(accent.g(), visuals.palette.fill.g);
assert_ne!(accent.b(), visuals.palette.fill.b);
}
#[test]
fn overlay_text_for_fill_tracks_fill_contrast() {
let dark_text = overlay_text_color_for_fill(
smithay::backend::renderer::Color32F::new(0.10, 0.12, 0.14, 1.0),
1.0,
);
let light_text = overlay_text_color_for_fill(
smithay::backend::renderer::Color32F::new(0.92, 0.95, 0.98, 1.0),
1.0,
);
assert!(super::color_luminance(dark_text) > super::color_luminance(light_text));
}
}