use std::collections::{HashMap, HashSet};
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use calloop::ping::Ping;
use eventline::warn;
use halley_config::RuntimeTuning;
use smithay::reexports::wayland_server::backend::ObjectId;
use super::monitor::camera::camera_controller;
use super::root::Halley;
use super::screenshot::screenshot_controller;
use crate::animation::AnimSpec;
use crate::compositor::activity::CommitActivity;
use crate::protocol::wayland::activation::ActivationRuntimeState;
const FIXED_ANIM_STATE_CHANGE_MS: u64 = 360;
const FIXED_ANIM_BOUNCE: f32 = 1.45;
pub(crate) struct RuntimeState {
pub(crate) tuning: RuntimeTuning,
pub(crate) surface_activity: HashMap<ObjectId, CommitActivity>,
pub(crate) exit_requested: bool,
pub(crate) started_at: Instant,
pub(crate) maintenance_dirty: bool,
pub(crate) screenshot_full_repaint_until_ms: u64,
pub(crate) maintenance_ping: Option<Ping>,
pub(crate) tty_redraw_all: bool,
pub(crate) tty_redraw_outputs: HashSet<String>,
pub(crate) tty_frame_callback_sequence: HashMap<String, u32>,
pub(crate) pending_drm_syncobj_surfaces: Arc<Mutex<Vec<ObjectId>>>,
pub(crate) activation: ActivationRuntimeState,
pub(crate) spawned_children: Vec<std::process::Child>,
}
pub(crate) struct RuntimeController<T> {
st: T,
}
pub(crate) fn runtime_controller<T>(st: T) -> RuntimeController<T> {
RuntimeController { st }
}
impl<T: Deref<Target = Halley>> Deref for RuntimeController<T> {
type Target = Halley;
fn deref(&self) -> &Self::Target {
self.st.deref()
}
}
impl<T: DerefMut<Target = Halley>> DerefMut for RuntimeController<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.st.deref_mut()
}
}
impl<T: Deref<Target = Halley>> RuntimeController<T> {
pub fn now_ms(&self, now: Instant) -> u64 {
now.duration_since(self.runtime.started_at).as_millis() as u64
}
pub(crate) fn debug_dump(&self) {}
pub fn exit_requested(&self) -> bool {
self.runtime.exit_requested
}
pub fn next_maintenance_deadline(&self, now: Instant) -> Option<Instant> {
if !self.model.focus_state.app_focused {
return None;
}
let now_ms = self.now_ms(now);
let next_ms = min_optional_deadlines([
focus_deadline_ms(self, now_ms),
resize_deadline_ms(self, now_ms),
spawn_deadline_ms(self, now_ms),
workspace_deadline_ms(self, now_ms),
interaction_deadline_ms(self, now_ms),
animation_deadline_ms(self, now_ms),
]);
next_ms.map(|at_ms| {
now.checked_add(std::time::Duration::from_millis(
at_ms.saturating_sub(now_ms),
))
.unwrap_or(now)
})
}
}
fn min_optional_deadlines<const N: usize>(deadlines: [Option<u64>; N]) -> Option<u64> {
deadlines.into_iter().flatten().min()
}
fn focus_deadline_ms(st: &Halley, now_ms: u64) -> Option<u64> {
min_optional_deadlines([
(st.model.focus_state.primary_interaction_focus.is_some()
&& st.model.focus_state.interaction_focus_until_ms > now_ms)
.then_some(st.model.focus_state.interaction_focus_until_ms),
st.input
.interaction_state
.pending_modal_focus_restore
.as_ref()
.map(|pending| pending.restore_at_ms),
])
}
fn resize_deadline_ms(st: &Halley, now_ms: u64) -> Option<u64> {
(st.input.interaction_state.resize_static_node.is_some()
&& st.input.interaction_state.resize_static_until_ms > now_ms)
.then_some(st.input.interaction_state.resize_static_until_ms)
}
fn spawn_deadline_ms(st: &Halley, now_ms: u64) -> Option<u64> {
let pending_spawn_activate_at_ms = st
.model
.spawn_state
.pending_spawn_activate_at_ms
.values()
.copied()
.min()
.filter(|&at_ms| at_ms > now_ms);
let pending_tiled_insert_reveal_at_ms = st
.model
.spawn_state
.pending_tiled_insert_reveal_at_ms
.values()
.copied()
.min();
let active_spawn_pan_at_ms = st.model.spawn_state.active_spawn_pan.and_then(|pan| {
[pan.pan_start_at_ms, pan.reveal_at_ms]
.into_iter()
.filter(|&at_ms| at_ms > now_ms)
.min()
});
let pending_pan_activate_at_ms = st
.model
.spawn_state
.pending_pan_activate
.map(|(_, at_ms)| at_ms)
.filter(|&at_ms| at_ms > now_ms);
min_optional_deadlines([
pending_spawn_activate_at_ms,
pending_tiled_insert_reveal_at_ms,
active_spawn_pan_at_ms,
pending_pan_activate_at_ms,
])
}
fn workspace_deadline_ms(st: &Halley, now_ms: u64) -> Option<u64> {
let active_transition_at_ms = st
.model
.workspace_state
.active_transitions
.values()
.map(|transition| transition.until_ms())
.min()
.filter(|&at_ms| at_ms > now_ms);
let primary_promote_cooldown_until_ms = st
.model
.workspace_state
.primary_promote_cooldown_until_ms
.values()
.copied()
.min()
.filter(|&at_ms| at_ms > now_ms);
let pending_silent_close_until_ms = st
.model
.workspace_state
.pending_silent_close_until_ms
.values()
.copied()
.min()
.filter(|&at_ms| at_ms > now_ms);
min_optional_deadlines([
active_transition_at_ms,
primary_promote_cooldown_until_ms,
pending_silent_close_until_ms,
])
}
fn interaction_deadline_ms(st: &Halley, now_ms: u64) -> Option<u64> {
let pending_core_click_deadline_ms = st
.input
.interaction_state
.pending_core_click
.as_ref()
.map(|pending| pending.deadline_ms)
.filter(|&deadline_ms| deadline_ms > now_ms);
let pending_collapsed_node_click_deadline_ms = st
.input
.interaction_state
.pending_collapsed_node_click
.as_ref()
.map(|pending| pending.deadline_ms)
.filter(|&deadline_ms| deadline_ms > now_ms);
let cluster_name_prompt_repeat_at_ms = st
.input
.interaction_state
.cluster_name_prompt_repeat
.as_ref()
.map(|repeat| repeat.next_repeat_ms);
let pending_screenshot_capture_at_ms = st
.input
.interaction_state
.pending_screenshot_capture
.as_ref()
.map(|pending| pending.execute_at_ms);
let inflight_screenshot_capture_at_ms = st
.input
.interaction_state
.inflight_screenshot_capture
.is_some()
.then_some(now_ms.saturating_add(33));
min_optional_deadlines([
pending_core_click_deadline_ms,
pending_collapsed_node_click_deadline_ms,
cluster_name_prompt_repeat_at_ms,
pending_screenshot_capture_at_ms,
st.input.interaction_state.cursor_override_until_ms,
inflight_screenshot_capture_at_ms,
])
}
fn animation_deadline_ms(st: &Halley, now_ms: u64) -> Option<u64> {
let bloom_pull_preview_at_ms =
crate::compositor::interaction::state::bloom_pull_preview_needs_animation(st)
.then_some(now_ms.saturating_add(16));
let cluster_overflow_reveal_at_ms = st
.model
.cluster_state
.cluster_overflow_reveal_started_at_ms
.iter()
.any(|(monitor, started_at_ms)| {
let visible_until_ms = st
.model
.cluster_state
.cluster_overflow_visible_until_ms
.get(monitor)
.copied();
visible_until_ms.is_some_and(|visible_until_ms| {
visible_until_ms > now_ms
&& (now_ms.saturating_sub(*started_at_ms) < 220
|| visible_until_ms.saturating_sub(now_ms) < 220)
})
})
.then_some(now_ms.saturating_add(16));
let cluster_overflow_promotion_at_ms = st
.model
.cluster_state
.cluster_overflow_promotion_anim
.values()
.any(|anim| now_ms < anim.reveal_at_ms)
.then_some(now_ms.saturating_add(16));
min_optional_deadlines([
bloom_pull_preview_at_ms,
cluster_overflow_reveal_at_ms,
cluster_overflow_promotion_at_ms,
])
}
impl<T: DerefMut<Target = Halley>> RuntimeController<T> {
pub fn apply_tuning(&mut self, mut tuning: RuntimeTuning) {
let prev_runtime_viewport = self.model.viewport;
let prev_config_viewport = self.runtime.tuning.viewport();
let prev_decorations = self.runtime.tuning.decorations;
let prev_font = self.runtime.tuning.font.clone();
let prev_input = self.runtime.tuning.input.clone();
let prev_physics_enabled = self.runtime.tuning.physics_enabled;
let prev_focus = self.last_input_surface_node();
let previous_output_names: std::collections::HashSet<String> = self
.model
.monitor_state
.monitors
.keys()
.cloned()
.chain(
self.runtime
.tuning
.tty_viewports
.iter()
.map(|v| v.connector.clone()),
)
.collect();
tuning.enforce_guards();
tuning.apply_process_env();
let next_viewport = tuning.viewport();
let logical_viewport_changed = prev_config_viewport.center != next_viewport.center
|| prev_config_viewport.size != next_viewport.size;
if logical_viewport_changed {
self.model.viewport = next_viewport;
self.model.zoom_ref_size = tuning.viewport_size;
self.model.camera_target_center = self.model.viewport.center;
self.model.camera_target_view_size = self.model.zoom_ref_size;
if prev_runtime_viewport.center != next_viewport.center
|| prev_runtime_viewport.size != next_viewport.size
{
self.input.interaction_state.viewport_pan_anim = None;
}
}
self.ui.render_state.animator.set_spec(AnimSpec {
state_change_ms: FIXED_ANIM_STATE_CHANGE_MS,
bounce: FIXED_ANIM_BOUNCE,
});
if prev_physics_enabled && !tuning.physics_enabled {
self.input.interaction_state.drag_authority_node = None;
self.input.interaction_state.physics_velocity.clear();
self.input.interaction_state.smoothed_render_pos.clear();
self.model.camera_target_center = self.model.viewport.center;
self.model.camera_target_view_size = self.model.zoom_ref_size;
}
let next_output_names: std::collections::HashSet<String> = previous_output_names
.iter()
.cloned()
.chain(tuning.tty_viewports.iter().map(|v| v.connector.clone()))
.collect();
let now = Instant::now();
let now_ms = self.now_ms(now);
for output_name in next_output_names {
if self
.runtime
.tuning
.focus_ring_for_output(output_name.as_str())
!= tuning.focus_ring_for_output(output_name.as_str())
{
self.model.focus_state.focus_ring_preview_until_ms.insert(
output_name,
now_ms.saturating_add(crate::compositor::focus::state::FOCUS_RING_PREVIEW_MS),
);
}
}
self.runtime.tuning = tuning;
let repeat_changed = self.runtime.tuning.input.repeat_rate != prev_input.repeat_rate
|| self.runtime.tuning.input.repeat_delay != prev_input.repeat_delay;
let keyboard_config_changed = self.runtime.tuning.input.keyboard != prev_input.keyboard;
if let Some(keyboard) = self.platform.seat.get_keyboard() {
if repeat_changed {
keyboard.change_repeat_info(
self.runtime.tuning.input.repeat_rate,
self.runtime.tuning.input.repeat_delay,
);
}
if keyboard_config_changed {
let xkb_config =
crate::backend::ResolvedXkbConfig::from_input(&self.runtime.tuning.input);
if let Err(err) = keyboard.set_xkb_config(self, xkb_config.as_smithay()) {
warn!(
"failed to apply keyboard layout='{}' variant='{}' options='{}' on reload: {}",
self.runtime.tuning.input.keyboard.layout,
self.runtime.tuning.input.keyboard.variant,
self.runtime.tuning.input.keyboard.options,
err
);
}
}
}
if self.runtime.tuning.font != prev_font {
self.ui.render_state.invalidate_ui_text_cache();
}
if self.runtime.tuning.decorations != prev_decorations {
self.ui.render_state.clear_window_offscreen_caches();
}
if !self.runtime.tuning.cursor.hide_while_typing {
self.input.interaction_state.cursor_hidden_by_typing = false;
}
self.refresh_xdg_decoration_mode();
self.request_maintenance();
if let Some(id) = prev_focus {
self.set_interaction_focus(Some(id), 30_000, now);
}
}
pub fn request_exit(&mut self) {
self.runtime.exit_requested = true;
}
#[inline]
pub fn request_maintenance(&mut self) {
self.runtime.maintenance_dirty = true;
self.runtime.tty_redraw_all = true;
if let Some(ping) = &self.runtime.maintenance_ping {
ping.ping();
}
}
#[inline]
pub fn run_maintenance_if_needed(&mut self, now: Instant) {
let due = self
.next_maintenance_deadline(now)
.is_some_and(|deadline| deadline <= now);
if self.runtime.maintenance_dirty || due {
self.run_maintenance(now);
}
}
#[inline]
pub fn run_maintenance(&mut self, now: Instant) {
self.runtime.maintenance_dirty = false;
if !self.model.focus_state.app_focused {
return;
}
crate::compositor::workspace::lifecycle::reconcile_surface_bindings(self);
let now_ms = now.duration_since(self.runtime.started_at).as_millis() as u64;
crate::protocol::wayland::activation::prune_expired(self, now, now_ms);
let _ = self.recent_top_node_active(now);
let pointer_contents_changed =
crate::compositor::interaction::pointer::update_pointer_contents_at_last_screen(
self, None, now,
);
if pointer_contents_changed {
if let Some((sx, sy)) = self.input.interaction_state.last_pointer_screen_global
&& let Some(output_name) = self.monitor_for_screen(sx, sy)
{
self.request_tty_redraw_for_monitor(output_name.as_str());
} else {
self.runtime.tty_redraw_all = true;
}
}
if let Some(pending) = self.input.interaction_state.pending_core_click.clone()
&& now_ms >= pending.deadline_ms
{
self.input.interaction_state.pending_core_click = None;
}
if let Some(pending) = self
.input
.interaction_state
.pending_collapsed_node_click
.clone()
&& now_ms >= pending.deadline_ms
{
self.input.interaction_state.pending_collapsed_node_click = None;
}
let _ = crate::compositor::clusters::system::cluster_system_controller(&mut **self)
.repeat_cluster_name_prompt_input_if_due(now_ms);
screenshot_controller(&mut **self).run_pending_screenshot_capture_if_due(now_ms);
if let Some(pending) = self
.input
.interaction_state
.pending_modal_focus_restore
.clone()
&& now_ms >= pending.restore_at_ms
{
self.input.interaction_state.pending_modal_focus_restore = None;
self.apply_wayland_focus_state(pending.target);
}
if self
.input
.interaction_state
.cursor_override_until_ms
.is_some_and(|until_ms| now_ms >= until_ms)
{
self.input.interaction_state.cursor_override_until_ms = None;
self.input.interaction_state.cursor_override_icon = None;
}
if self.has_any_active_cluster_workspace() {
let active_monitors = self
.model
.cluster_state
.active_cluster_workspaces
.keys()
.cloned()
.collect::<Vec<_>>();
for monitor in active_monitors {
self.layout_active_cluster_workspace_for_monitor(monitor.as_str(), now_ms);
}
}
if let Some(fid) = self.model.focus_state.primary_interaction_focus
&& now_ms >= self.model.focus_state.interaction_focus_until_ms
{
let keep = self.model.field.node(fid).is_some_and(|n| {
self.model.field.is_visible(fid) && n.kind == halley_core::field::NodeKind::Surface
});
if keep {
self.model.focus_state.interaction_focus_until_ms = now_ms.saturating_add(30_000);
} else {
self.set_interaction_focus(None, 0, now);
}
}
if crate::protocol::wayland::session_lock::session_lock_active(self) {
crate::protocol::wayland::session_lock::reassert_keyboard_focus_if_drifted(self);
} else if self.model.focus_state.primary_interaction_focus.is_none()
&& self.model.monitor_state.layer_keyboard_focus.is_some()
{
crate::compositor::monitor::layer_shell::reassert_layer_surface_keyboard_focus_if_drifted(self);
}
self.model
.workspace_state
.active_transitions
.retain(|_, transition| transition.is_active(now_ms));
self.model
.workspace_state
.primary_promote_cooldown_until_ms
.retain(|_, &mut until| until > now_ms);
let expired_silent_close = self
.model
.workspace_state
.pending_silent_close_until_ms
.iter()
.filter_map(|(&id, &until)| (until <= now_ms).then_some(id))
.collect::<Vec<_>>();
for id in expired_silent_close {
self.model
.workspace_state
.pending_silent_close_until_ms
.remove(&id);
if !self.model.field.is_cluster_member(id)
&& let Some(node) = self.model.field.node_mut(id)
{
node.visibility
.clear(halley_core::field::Visibility::HIDDEN_BY_CLUSTER);
}
}
let alive_ids: std::collections::HashSet<_> =
self.model.field.node_ids_all().into_iter().collect();
self.model
.carry_state
.carry_zone_hint
.retain(|id, _| alive_ids.contains(id));
self.model
.carry_state
.carry_zone_last_change_ms
.retain(|id, _| alive_ids.contains(id));
self.model
.carry_state
.carry_zone_pending
.retain(|id, _| alive_ids.contains(id));
self.model
.carry_state
.carry_zone_pending_since_ms
.retain(|id, _| alive_ids.contains(id));
self.model
.carry_state
.carry_activation_anim_armed
.retain(|id| alive_ids.contains(id));
self.model
.carry_state
.carry_state_hold
.retain(|id, _| alive_ids.contains(id));
self.model
.focus_state
.last_surface_focus_ms
.retain(|id, _| alive_ids.contains(id));
self.model
.focus_state
.overlap_raise_order
.retain(|id, _| alive_ids.contains(id));
self.model
.workspace_state
.manual_collapsed_nodes
.retain(|id| alive_ids.contains(id));
self.model
.spawn_state
.pending_tiled_insert_reveal_at_ms
.retain(|id, _| alive_ids.contains(id));
self.model
.spawn_state
.pending_tiled_insert_preserve_focus
.retain(|id| alive_ids.contains(id));
self.model
.cluster_state
.cluster_overflow_promotion_anim
.retain(|_, anim| alive_ids.contains(&anim.member_id) && now_ms < anim.reveal_at_ms);
self.process_pending_spawn_activations(now, now_ms);
let resize_settling = self
.input
.interaction_state
.resize_static_node
.is_some_and(|_| now_ms < self.input.interaction_state.resize_static_until_ms);
if resize_settling
&& let (Some(id), Some(lock_pos)) = (
self.input.interaction_state.resize_static_node,
self.input.interaction_state.resize_static_lock_pos,
)
&& let Some(n) = self.model.field.node(id)
&& ((n.pos.x - lock_pos.x).abs() > 0.05 || (n.pos.y - lock_pos.y).abs() > 0.05)
{
let _ = self.model.field.carry(id, lock_pos);
}
if self
.input
.interaction_state
.resize_static_node
.is_some_and(|_| now_ms >= self.input.interaction_state.resize_static_until_ms)
{
self.input.interaction_state.resize_static_node = None;
self.input.interaction_state.resize_static_lock_pos = None;
self.input.interaction_state.resize_static_until_ms = 0;
}
if !self.input.interaction_state.suspend_state_checks {
crate::compositor::interaction::state::enforce_pan_dominant_zone_states(self, now_ms);
crate::compositor::carry::state::enforce_carry_zone_states(self);
}
if let Some(id) = self.input.interaction_state.resize_active {
let _ = self.model.field.touch(id, now_ms);
let _ = self
.model
.field
.set_decay_level(id, halley_core::decay::DecayLevel::Hot);
}
if self.input.interaction_state.resize_active.is_none()
&& !(self.input.interaction_state.resize_static_node.is_some()
&& now_ms < self.input.interaction_state.resize_static_until_ms)
{
camera_controller(&mut **self).update_zoom_live_surface_sizes();
}
let cluster_policy = halley_core::cluster_policy::ClusterPolicy {
enabled: false,
distance_px: self.runtime.tuning.cluster_distance_px,
dwell_ms: self.runtime.tuning.cluster_dwell_ms,
..Default::default()
};
let model = &mut self.model;
let _ = halley_core::cluster_policy::tick_cluster_formation(
&mut model.field,
now_ms,
cluster_policy,
&mut model.cluster_state.cluster_form_state,
);
self.enforce_single_primary_active_unit();
if !self.input.interaction_state.suspend_state_checks
&& self.input.interaction_state.resize_active.is_none()
{
self.resolve_surface_overlap();
}
self.restore_pan_return_active_focus(now);
let animations_enabled = self.runtime.tuning.animations_enabled();
let crate::compositor::root::Halley { model, ui, .. } = &mut **self;
if animations_enabled {
ui.render_state.animator.observe_field(&model.field, now);
}
}
}