use std::sync::{Arc, OnceLock};
use dais_core::config::Config;
use dais_core::monitor::{MonitorInfo, MonitorManager};
use dais_document::page::RenderSize;
use dais_document::render_pipeline::FALLBACK_RENDER_SIZE;
#[derive(Debug, Clone)]
pub enum DisplayMode {
Dual { audience_monitor: MonitorInfo },
Single,
ScreenShare,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SingleMonitorView {
Hud,
Split,
}
impl SingleMonitorView {
pub fn from_config(value: &str) -> Self {
if value.eq_ignore_ascii_case("split") { Self::Split } else { Self::Hud }
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisplayHints {
pub force_single: bool,
pub force_screen_share: bool,
}
fn app_icon() -> Option<Arc<egui::IconData>> {
static ICON: OnceLock<Option<Arc<egui::IconData>>> = OnceLock::new();
ICON.get_or_init(|| {
match eframe::icon_data::from_png_bytes(include_bytes!("../assets/dais.png")) {
Ok(icon) => Some(Arc::new(icon)),
Err(err) => {
tracing::warn!("Failed to load app icon from bundled assets/dais.png: {err}");
None
}
}
})
.clone()
}
pub fn with_app_icon(builder: egui::ViewportBuilder) -> egui::ViewportBuilder {
if let Some(icon) = app_icon() { builder.with_icon(icon) } else { builder }
}
pub struct DisplayModeResult {
pub mode: DisplayMode,
pub warnings: Vec<String>,
pub audience_reassignment: Option<AudienceReassignmentPrompt>,
}
#[derive(Debug, Clone)]
pub struct AudienceReassignmentPrompt {
pub missing_selector: String,
pub attempted_fallback: Option<MonitorInfo>,
pub available_monitors: Vec<MonitorInfo>,
}
pub fn determine_display_mode(
hints: DisplayHints,
config: &Config,
monitor_mgr: &dyn MonitorManager,
) -> DisplayModeResult {
let mut warnings = Vec::new();
if hints.force_single {
tracing::info!("Single mode requested via --single flag");
return DisplayModeResult {
mode: DisplayMode::Single,
warnings,
audience_reassignment: None,
};
}
if hints.force_screen_share {
tracing::info!("Screen-share mode requested via --screen-share flag");
return DisplayModeResult {
mode: DisplayMode::ScreenShare,
warnings,
audience_reassignment: None,
};
}
let config_mode = config.display.mode.to_lowercase();
let monitors = monitor_mgr.available_monitors();
log_monitor_topology(&monitors);
let mut audience_reassignment = None;
let mode = match config_mode.as_str() {
"single" => {
tracing::info!("Single mode set in config");
DisplayMode::Single
}
"screen-share" | "screenshare" | "screen_share" => {
tracing::info!("Screen-share mode set in config");
DisplayMode::ScreenShare
}
_ => {
let (mode, prompt) = resolve_dual_mode(config, &monitors, monitor_mgr, &mut warnings);
audience_reassignment = prompt;
mode
}
};
DisplayModeResult { mode, warnings, audience_reassignment }
}
fn resolve_dual_mode(
config: &Config,
monitors: &[MonitorInfo],
monitor_mgr: &dyn MonitorManager,
warnings: &mut Vec<String>,
) -> (DisplayMode, Option<AudienceReassignmentPrompt>) {
let audience_name = &config.display.audience_monitor;
if audience_name != "auto" && !audience_name.is_empty() {
if let Some(mon) = monitor_mgr.find_by_selector(audience_name) {
tracing::info!(
"Using configured audience monitor '{}' -> {} '{}'",
audience_name,
mon.id,
mon.name
);
return (DisplayMode::Dual { audience_monitor: mon }, None);
}
let available = monitors.iter().map(|m| m.name.as_str()).collect::<Vec<_>>().join(", ");
let msg = format!(
"Configured audience monitor '{audience_name}' not found. Available: {available}",
);
tracing::warn!("{msg}");
warnings.push(msg);
let attempted_fallback = monitor_mgr.secondary_monitor();
let available_monitors = monitors.to_vec();
let prompt = Some(AudienceReassignmentPrompt {
missing_selector: audience_name.clone(),
attempted_fallback: attempted_fallback.clone(),
available_monitors,
});
if let Some(secondary) = attempted_fallback {
tracing::info!(
"Dual mode fallback: audience on '{}' ({}x{} @ {:?})",
secondary.name,
secondary.size.0,
secondary.size.1,
secondary.position
);
return (DisplayMode::Dual { audience_monitor: secondary }, prompt);
}
let msg = "Single monitor detected — expected dual. Using single mode.".to_string();
tracing::info!("{msg}");
warnings.push(msg);
return (DisplayMode::Single, prompt);
}
if let Some(secondary) = monitor_mgr.secondary_monitor() {
tracing::info!(
"Dual mode: audience on '{}' ({}x{} @ {:?})",
secondary.name,
secondary.size.0,
secondary.size.1,
secondary.position
);
return (DisplayMode::Dual { audience_monitor: secondary }, None);
}
let msg = "Single monitor detected — expected dual. Using single mode.".to_string();
tracing::info!("{msg}");
warnings.push(msg);
(DisplayMode::Single, None)
}
#[allow(clippy::cast_precision_loss)]
pub fn audience_viewport_builder(mode: &DisplayMode) -> egui::ViewportBuilder {
match mode {
DisplayMode::Dual { audience_monitor } => {
tracing::debug!(
"Audience viewport: fullscreen on '{}' at ({}, {})",
audience_monitor.name,
audience_monitor.position.0,
audience_monitor.position.1,
);
with_app_icon(egui::ViewportBuilder::default())
.with_title("Dais — Audience")
.with_fullscreen(true)
.with_position(egui::pos2(
audience_monitor.position.0 as f32,
audience_monitor.position.1 as f32,
))
}
DisplayMode::Single => {
with_app_icon(egui::ViewportBuilder::default())
.with_title("Dais — Audience")
.with_inner_size(egui::vec2(1280.0, 720.0))
}
DisplayMode::ScreenShare => with_app_icon(egui::ViewportBuilder::default())
.with_title("Dais — Audience")
.with_inner_size(egui::vec2(1280.0, 720.0)),
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
pub fn presenter_viewport_builder(
config: &Config,
monitor_mgr: &dyn MonitorManager,
window_size: egui::Vec2,
) -> egui::ViewportBuilder {
let presenter_selector = config.display.presenter_monitor.trim();
let monitor = if presenter_selector.is_empty() || presenter_selector == "auto" {
monitor_mgr.primary_monitor()
} else {
monitor_mgr.find_by_selector(presenter_selector).or_else(|| monitor_mgr.primary_monitor())
};
let builder = with_app_icon(egui::ViewportBuilder::default())
.with_title("Dais — Presenter Console")
.with_resizable(true)
.with_maximized(true);
let Some(monitor) = monitor else {
return builder;
};
if monitor.size.0 == 0 || monitor.size.1 == 0 {
return builder;
}
let (_logical_work_x, _logical_work_y, logical_work_w, logical_work_h) =
monitor.logical_work_area();
let (logical_monitor_w, logical_monitor_h) = monitor.logical_size();
let usable_w = if monitor.work_area.2 > 0 { logical_work_w } else { logical_monitor_w };
let usable_h = if monitor.work_area.3 > 0 { logical_work_h } else { logical_monitor_h };
let max_w = (usable_w as f32 - 20.0).max(640.0);
let max_h = (usable_h as f32 - 60.0).max(480.0);
let target_w = window_size.x.min(max_w);
let target_h = window_size.y.min(max_h);
let work_x = if monitor.work_area.2 > 0 {
monitor.work_area.0 as f32 / monitor.scale_factor as f32
} else {
monitor.position.0 as f32 / monitor.scale_factor as f32
};
let work_y = if monitor.work_area.3 > 0 {
monitor.work_area.1 as f32 / monitor.scale_factor as f32
} else {
monitor.position.1 as f32 / monitor.scale_factor as f32
};
let x = work_x + ((usable_w as f32 - target_w) / 2.0).max(0.0);
let top_margin: f32 = 24.0;
let y = work_y + top_margin.min((usable_h as f32 - target_h).max(0.0));
builder.with_inner_size(egui::vec2(target_w, target_h)).with_position(egui::pos2(x, y))
}
pub fn audience_render_size(mode: &DisplayMode) -> RenderSize {
match mode {
DisplayMode::Dual { audience_monitor }
if audience_monitor.size.0 > 0 && audience_monitor.size.1 > 0 =>
{
RenderSize { width: audience_monitor.size.0, height: audience_monitor.size.1 }
}
_ => FALLBACK_RENDER_SIZE,
}
}
fn log_monitor_topology(monitors: &[MonitorInfo]) {
tracing::info!("Detected {} monitor(s):", monitors.len());
for m in monitors {
tracing::info!(
" {} '{}' — {}x{} @ ({},{}) scale={:.2} {}",
m.id,
m.name,
m.size.0,
m.size.1,
m.position.0,
m.position.1,
m.scale_factor,
if m.is_primary { "[primary]" } else { "" },
);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMonitorManager {
monitors: Vec<MonitorInfo>,
}
impl MonitorManager for MockMonitorManager {
fn available_monitors(&self) -> Vec<MonitorInfo> {
self.monitors.clone()
}
}
fn single_monitor() -> MockMonitorManager {
MockMonitorManager {
monitors: vec![MonitorInfo {
id: "m1".into(),
name: "Primary".into(),
position: (0, 0),
size: (1920, 1080),
work_area: (0, 0, 1920, 1040),
scale_factor: 1.0,
is_primary: true,
}],
}
}
fn dual_monitors() -> MockMonitorManager {
MockMonitorManager {
monitors: vec![
MonitorInfo {
id: "m1".into(),
name: "Primary".into(),
position: (0, 0),
size: (1920, 1080),
work_area: (0, 0, 1920, 1040),
scale_factor: 1.0,
is_primary: true,
},
MonitorInfo {
id: "m2".into(),
name: "DELL U2718Q".into(),
position: (1920, 0),
size: (3840, 2160),
work_area: (1920, 0, 3840, 2120),
scale_factor: 2.0,
is_primary: false,
},
],
}
}
#[test]
fn cli_single_overrides_everything() {
let hints = DisplayHints { force_single: true, force_screen_share: false };
let config = Config::default();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Single));
assert!(result.audience_reassignment.is_none());
}
#[test]
fn cli_screen_share_overrides_everything() {
let hints = DisplayHints { force_single: false, force_screen_share: true };
let config = Config::default();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::ScreenShare));
assert!(result.audience_reassignment.is_none());
}
#[test]
fn auto_dual_with_two_monitors() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let config = Config::default();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Dual { .. }));
assert!(result.audience_reassignment.is_none());
if let DisplayMode::Dual { audience_monitor } = result.mode {
assert_eq!(audience_monitor.name, "DELL U2718Q");
}
}
#[test]
fn auto_falls_back_to_single_with_one_monitor() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let config = Config::default();
let mgr = single_monitor();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Single));
assert!(!result.warnings.is_empty());
assert!(result.audience_reassignment.is_none());
}
#[test]
fn configured_monitor_name_matches() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let mut config = Config::default();
config.display.audience_monitor = "DELL U2718Q".to_string();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Dual { .. }));
assert!(result.audience_reassignment.is_none());
}
#[test]
fn configured_monitor_numeric_selector_matches() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let mut config = Config::default();
config.display.audience_monitor = "2".to_string();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Dual { .. }));
assert!(result.audience_reassignment.is_none());
if let DisplayMode::Dual { audience_monitor } = result.mode {
assert_eq!(audience_monitor.name, "DELL U2718Q");
}
}
#[test]
fn configured_monitor_name_mismatch_falls_back() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let mut config = Config::default();
config.display.audience_monitor = "NONEXISTENT".to_string();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Dual { .. }));
assert!(!result.warnings.is_empty()); let prompt = result.audience_reassignment.expect("missing reassignment prompt");
assert_eq!(prompt.missing_selector, "NONEXISTENT");
assert!(prompt.attempted_fallback.is_some());
assert_eq!(prompt.available_monitors.len(), 2);
}
#[test]
fn configured_monitor_mismatch_on_one_monitor_can_reassign_to_primary() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let mut config = Config::default();
config.display.audience_monitor = "NONEXISTENT".to_string();
let mgr = single_monitor();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::Single));
let prompt = result.audience_reassignment.expect("missing reassignment prompt");
assert!(prompt.attempted_fallback.is_none());
assert_eq!(prompt.available_monitors.len(), 1);
assert!(prompt.available_monitors[0].is_primary);
}
#[test]
fn config_screen_share_mode() {
let hints = DisplayHints { force_single: false, force_screen_share: false };
let mut config = Config::default();
config.display.mode = "screen-share".to_string();
let mgr = dual_monitors();
let result = determine_display_mode(hints, &config, &mgr);
assert!(matches!(result.mode, DisplayMode::ScreenShare));
assert!(result.audience_reassignment.is_none());
}
#[test]
fn audience_render_size_uses_monitor_size_when_available() {
let mgr = dual_monitors();
let mode = DisplayMode::Dual { audience_monitor: mgr.monitors[1].clone() };
let size = audience_render_size(&mode);
assert_eq!(size.width, 3840);
assert_eq!(size.height, 2160);
}
#[test]
fn audience_render_size_falls_back_when_unavailable() {
let mode = DisplayMode::ScreenShare;
let size = audience_render_size(&mode);
assert_eq!(size.width, FALLBACK_RENDER_SIZE.width);
assert_eq!(size.height, FALLBACK_RENDER_SIZE.height);
}
#[test]
fn presenter_viewport_uses_primary_monitor_by_default() {
let config = Config::default();
let mgr = dual_monitors();
let builder = presenter_viewport_builder(&config, &mgr, egui::vec2(1400.0, 900.0));
let debug = format!("{builder:?}");
assert!(debug.contains("Presenter Console"));
}
}