use std::sync::{Arc, RwLock};
use dais_core::bus::CommandSender;
use dais_core::config::Config;
use dais_core::keybindings::KeybindingMap;
use dais_core::state::PresentationState;
use dais_document::cache::PageCache;
use dais_document::page::RenderSize;
use dais_document::render_pipeline::{FALLBACK_RENDER_SIZE, RenderPipeline};
use dais_document::source::DocumentSource;
use dais_engine::engine::PresentationEngine;
use crate::audience::AudienceWindow;
use crate::display_mode::{self, AudienceReassignmentPrompt, DisplayMode, SingleMonitorView};
use crate::input::InputHandler;
use crate::presenter::PresenterConsole;
use crate::presenter::hud::HudOverlay;
use crate::widgets::ToastManager;
pub struct DaisApp {
engine: PresentationEngine,
shared_state: Arc<RwLock<PresentationState>>,
cache: PageCache,
pipeline: RenderPipeline,
presenter: PresenterConsole,
hud: HudOverlay,
audience: AudienceWindow,
sender: CommandSender,
display_mode: DisplayMode,
single_monitor_view: SingleMonitorView,
audience_reassignment: Option<AudienceReassignmentPrompt>,
toast_manager: ToastManager,
}
const MAX_ZOOM_RENDER_DIMENSION: u32 = 4320;
impl DaisApp {
pub fn new(
engine: PresentationEngine,
shared_state: Arc<RwLock<PresentationState>>,
doc: Arc<dyn DocumentSource>,
sender: CommandSender,
config: &Config,
display_mode: DisplayMode,
) -> Self {
let keybindings = KeybindingMap::from_full_config(config);
let input = InputHandler::new(sender.clone(), keybindings);
let presenter = PresenterConsole::new(input);
let audience = AudienceWindow::new();
let cache = PageCache::new(64);
let pipeline = RenderPipeline::new(doc, 2);
Self {
engine,
shared_state,
cache,
pipeline,
presenter,
hud: HudOverlay::new(),
audience,
sender,
display_mode,
single_monitor_view: SingleMonitorView::from_config(
&config.display.single_monitor_view,
),
audience_reassignment: None,
toast_manager: ToastManager::new(),
}
}
pub fn toast_manager_mut(&mut self) -> &mut ToastManager {
&mut self.toast_manager
}
pub fn set_audience_reassignment(&mut self, prompt: Option<AudienceReassignmentPrompt>) {
self.audience_reassignment = prompt;
}
}
impl eframe::App for DaisApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let should_quit = self.engine.tick();
if should_quit {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
self.pipeline.poll_results(&mut self.cache);
let state = self.shared_state.read().map_or_else(
|e| {
tracing::error!("Failed to read state: {e}");
PresentationState::new(0, Vec::new())
},
|s| s.clone(),
);
let presenter_size = FALLBACK_RENDER_SIZE;
let base_audience_size = display_mode::audience_render_size(&self.display_mode);
let audience_size = effective_audience_render_size(&state, base_audience_size);
self.pipeline.prefetch_neighborhood(
state.current_page,
state.total_pages,
presenter_size,
&mut self.cache,
);
self.pipeline.ensure_rendered(state.audience_page(), base_audience_size, &mut self.cache);
self.pipeline.ensure_rendered(state.audience_page(), audience_size, &mut self.cache);
if state.overview_visible {
for group in &state.slide_groups {
if let Some(&first_page) = group.pages.first() {
self.pipeline.ensure_rendered(first_page, presenter_size, &mut self.cache);
}
}
}
if state.timer.running {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
} else {
ctx.request_repaint_after(std::time::Duration::from_millis(250));
}
if matches!(self.display_mode, DisplayMode::Single) {
let render_size = effective_audience_render_size(&state, FALLBACK_RENDER_SIZE);
let active_view = if state.presentation_mode {
match self.single_monitor_view {
SingleMonitorView::Hud => SingleMonitorView::Split,
SingleMonitorView::Split => SingleMonitorView::Hud,
}
} else {
self.single_monitor_view
};
match active_view {
SingleMonitorView::Hud => {
let input = self.presenter.input_mut();
self.hud.show(ctx, &state, &mut self.cache, &self.sender, input, render_size);
}
SingleMonitorView::Split => {
self.presenter.show_single_monitor_split(
ctx,
&state,
&mut self.cache,
&self.sender,
render_size,
);
}
}
self.show_audience_reassignment_prompt(ctx);
self.toast_manager.show(ctx);
return;
}
let is_runtime_screen_share = state.screen_share_mode;
self.presenter.show(ctx, &state, &mut self.cache, &self.sender);
let viewport_builder = if is_runtime_screen_share {
display_mode::with_app_icon(egui::ViewportBuilder::default())
.with_title("Dais — Audience")
.with_inner_size(egui::vec2(1280.0, 720.0))
.with_fullscreen(false)
.with_resizable(true)
} else {
display_mode::audience_viewport_builder(&self.display_mode)
};
let shared = self.shared_state.clone();
let audience = &mut self.audience;
let cache = &mut self.cache;
let shared_ref = &shared;
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of("audience"),
viewport_builder,
|ctx, _class| {
audience.show(ctx, shared_ref, cache, audience_size);
},
);
self.show_audience_reassignment_prompt(ctx);
self.toast_manager.show(ctx);
}
}
impl DaisApp {
fn show_audience_reassignment_prompt(&mut self, ctx: &egui::Context) {
let Some(prompt) = self.audience_reassignment.clone() else {
return;
};
egui::Window::new("Audience Monitor Changed")
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
.show(ctx, |ui| {
ui.set_min_width(420.0);
ui.label(format!(
"Configured audience monitor '{}' is not available.",
prompt.missing_selector
));
if let Some(fallback) = &prompt.attempted_fallback {
ui.label(format!(
"Dais is currently using '{}' as a fallback for this session.",
fallback.name
));
} else {
ui.label("No audience monitor fallback was available, so Dais switched to single-monitor mode.");
}
ui.separator();
ui.label("Choose the audience output for this session:");
let mut dismiss = false;
let fallback_id = prompt.attempted_fallback.as_ref().map(|monitor| monitor.id.as_str());
let alternative_monitors = prompt
.available_monitors
.iter()
.filter(|monitor| Some(monitor.id.as_str()) != fallback_id)
.cloned()
.collect::<Vec<_>>();
if alternative_monitors.is_empty() {
ui.label(
egui::RichText::new("No alternate audience monitors were detected.")
.small()
.color(egui::Color32::GRAY),
);
}
for monitor in &alternative_monitors {
let primary = if monitor.is_primary { ", primary" } else { "" };
let label = format!("Use {} ({}{primary})", monitor.name, monitor.id);
if ui.button(label).clicked() {
self.display_mode =
DisplayMode::Dual { audience_monitor: monitor.clone() };
self.toast_manager.push(
crate::widgets::toast::ToastLevel::Info,
format!("Audience moved to '{}'", monitor.name),
);
dismiss = true;
}
}
ui.horizontal(|ui| {
if let Some(fallback) = &prompt.attempted_fallback {
if ui.button("Keep current fallback").clicked() {
self.display_mode =
DisplayMode::Dual { audience_monitor: fallback.clone() };
self.toast_manager.push(
crate::widgets::toast::ToastLevel::Info,
format!("Keeping fallback audience monitor '{}'", fallback.name),
);
dismiss = true;
}
if ui.button("Use single-monitor mode").clicked() {
self.display_mode = DisplayMode::Single;
self.toast_manager.push(
crate::widgets::toast::ToastLevel::Info,
"Audience reassigned to single-monitor mode",
);
dismiss = true;
}
} else if ui.button("Stay in single-monitor mode").clicked() {
self.display_mode = DisplayMode::Single;
self.toast_manager.push(
crate::widgets::toast::ToastLevel::Info,
"Keeping single-monitor mode",
);
dismiss = true;
}
});
ui.label(
egui::RichText::new(
"This updates the current session only. You can make it permanent in `dais.toml` later.",
)
.small()
.color(egui::Color32::GRAY),
);
if dismiss {
self.audience_reassignment = None;
}
});
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn effective_audience_render_size(state: &PresentationState, base_size: RenderSize) -> RenderSize {
let Some(region) = state.zoom_region.as_ref().filter(|_| state.zoom_active) else {
return base_size;
};
let multiplier = region.factor.clamp(1.0, 3.0);
let width = ((base_size.width as f32) * multiplier).round() as u32;
let height = ((base_size.height as f32) * multiplier).round() as u32;
RenderSize {
width: width.clamp(base_size.width, MAX_ZOOM_RENDER_DIMENSION),
height: height.clamp(base_size.height, MAX_ZOOM_RENDER_DIMENSION),
}
}