Skip to main content

zng_ext_window/
monitor.rs

1use core::fmt;
2use std::sync::Arc;
3
4use zng_app::event::{event, event_args};
5use zng_app::view_process::raw_events::RAW_MONITORS_CHANGED_EVENT;
6use zng_app::window::{MonitorId, WindowId};
7use zng_app_context::app_local;
8use zng_layout::unit::{
9    Dip, DipRect, DipSize, DipToPx, Factor, FactorUnits, Frequency, FrequencyUnits as _, Px, PxDensity, PxPoint, PxRect, PxSize, PxToDip,
10};
11use zng_txt::{ToTxt, Txt};
12use zng_unique_id::IdMap;
13use zng_var::{Var, VarValue, impl_from_and_into_var, var};
14use zng_view_api::window::VideoMode;
15
16use crate::WINDOWS;
17
18app_local! {
19    static MONITORS_SV: MonitorsService = MonitorsService::new();
20}
21
22/// Monitors service.
23///
24/// List monitor screens and configure the pixel density of a given monitor.
25///
26/// # Uses
27///
28/// Uses of this service:
29///
30/// #### Start Position
31///
32/// Windows are positioned on a virtual screen that overlaps all monitors, but all position configuration is done relative to
33/// an specific parent monitor, it is important to track the parent monitor as it defines properties that affect the layout of the window.
34/// This service is used to provide information to implement this feature.
35///
36/// #### Fullscreen
37///
38/// To set a window to fullscreen a monitor must be selected, by default it can be the one the window is at but
39/// the users may want to select a monitor. To enter fullscreen exclusive the video mode must also be decided, all video
40/// modes supported by the monitor are available in the [`MonitorInfo`] value.
41///
42/// #### Real-Size Preview
43///
44/// Some apps, like image editors, may implement a feature where the user can preview the *real* dimensions of
45/// the content they are editing, to accurately implement this you must known the real dimensions of the monitor screen,
46/// unfortunately this information is not provided by display drivers. You can ask the user to measure their screen and
47/// set the pixel density for the screen using the [`density`] variable, this value is then available in the [`LayoutMetrics`]
48/// for the next layout. If not set, the default is `96.0ppi`.
49///
50/// [`density`]: MonitorInfo::density
51/// [`scale_factor`]: MonitorInfo::scale_factor
52/// [`LayoutMetrics`]: zng_layout::context::LayoutMetrics
53pub struct MONITORS;
54impl MONITORS {
55    /// Get monitor info.
56    ///
57    /// Returns `None` if the monitor was not found or the app is running in headless mode without renderer.
58    pub fn monitor(&self, monitor_id: MonitorId) -> Option<MonitorInfo> {
59        MONITORS_SV.read().monitors.with(|m| m.get(&monitor_id).cloned())
60    }
61
62    /// List all available monitors.
63    ///
64    /// Is empty if no monitor was found or the app is running in headless mode without renderer.
65    pub fn available_monitors(&self) -> Var<Vec<MonitorInfo>> {
66        MONITORS_SV.read().monitors.map(|w| {
67            let mut list: Vec<_> = w.values().cloned().collect();
68            list.sort_by(|a, b| a.name.with(|a| b.name.with(|b| a.cmp(b))));
69            list
70        })
71    }
72
73    /// Gets the monitor info marked as primary.
74    pub fn primary_monitor(&self) -> Var<Option<MonitorInfo>> {
75        MONITORS_SV
76            .read()
77            .monitors
78            .map(|w| w.values().find(|m| m.is_primary().get()).cloned())
79    }
80}
81
82struct MonitorsService {
83    monitors: Var<IdMap<MonitorId, MonitorInfo>>,
84}
85impl MonitorsService {
86    fn new() -> Self {
87        // track monitors
88        RAW_MONITORS_CHANGED_EVENT
89            .hook(|args| {
90                let mut available_monitors: IdMap<_, _> = args.available_monitors.iter().cloned().collect();
91                let event_ts = args.timestamp;
92                let event_propagation = args.propagation.clone();
93
94                MONITORS_SV.read().monitors.modify(move |m| {
95                    let mut removed = vec![];
96                    let mut changed = vec![];
97
98                    m.retain(|key, value| {
99                        if let Some(new) = available_monitors.remove(key) {
100                            if value.update(new) {
101                                tracing::trace!(
102                                    "monitor update, {:?} {:?}",
103                                    key,
104                                    (&value.name.get(), value.position.get(), value.size.get(), value.density.get())
105                                );
106                                changed.push(*key);
107                            }
108                            true
109                        } else {
110                            tracing::trace!(
111                                "monitor removed, {:?} {:?}",
112                                key,
113                                (&value.name.get(), value.position.get(), value.size.get(), value.density.get())
114                            );
115                            removed.push(*key);
116                            false
117                        }
118                    });
119
120                    let mut added = Vec::with_capacity(available_monitors.len());
121
122                    for (id, info) in available_monitors {
123                        added.push(id);
124                        tracing::trace!(
125                            "monitor added, {:?} {:?}",
126                            id,
127                            (&info.name, info.position, info.size, info.scale_factor)
128                        );
129                        m.insert(id, MonitorInfo::from_gen(id, info));
130                    }
131
132                    if !removed.is_empty() || !added.is_empty() || !changed.is_empty() {
133                        let args = MonitorsChangedArgs::new(event_ts, event_propagation, removed, added, changed);
134                        MONITORS_CHANGED_EVENT.notify(args);
135                    }
136                });
137                true
138            })
139            .perm();
140
141        Self {
142            monitors: var(IdMap::new()),
143        }
144    }
145}
146
147/// "Monitor" configuration used by windows in [headless mode].
148///
149/// [headless mode]: zng_app::window::WindowMode::is_headless
150#[derive(Clone, Copy, PartialEq)]
151#[non_exhaustive]
152pub struct HeadlessMonitor {
153    /// The scale factor used for the headless layout and rendering.
154    ///
155    /// If set to `None`, falls back to the [`parent`] scale-factor, or `1.0` if the headless window has not parent.
156    ///
157    /// `None` by default.
158    ///
159    /// [`parent`]: crate::WindowVars::parent
160    pub scale_factor: Option<Factor>,
161
162    /// Size of the imaginary monitor screen that contains the headless window.
163    ///
164    /// This is used to calculate relative lengths in the window size definition and is defined in
165    /// layout pixels instead of device like in a real monitor info.
166    ///
167    /// `(11608, 8708)` by default.
168    pub size: DipSize,
169
170    /// Pixel density used for the headless layout and rendering.
171    pub density: PxDensity,
172}
173impl fmt::Debug for HeadlessMonitor {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        if f.alternate() || self.density != PxDensity::default() {
176            f.debug_struct("HeadlessMonitor")
177                .field("scale_factor", &self.scale_factor)
178                .field("screen_size", &self.size)
179                .field("density", &self.density)
180                .finish()
181        } else {
182            write!(f, "({:?}, ({}, {}))", self.scale_factor, self.size.width, self.size.height)
183        }
184    }
185}
186impl HeadlessMonitor {
187    /// New with custom size at `None` scale.
188    pub fn new(size: DipSize) -> Self {
189        HeadlessMonitor {
190            scale_factor: None,
191            size,
192            density: PxDensity::default(),
193        }
194    }
195
196    /// New with custom size and scale.
197    pub fn new_scaled(size: DipSize, scale_factor: Factor) -> Self {
198        HeadlessMonitor {
199            scale_factor: Some(scale_factor),
200            size,
201            density: PxDensity::default(),
202        }
203    }
204
205    /// New with default size `(11608, 8708)` and custom scale.
206    pub fn new_scale(scale_factor: Factor) -> Self {
207        HeadlessMonitor {
208            scale_factor: Some(scale_factor),
209            ..Self::default()
210        }
211    }
212}
213impl Default for HeadlessMonitor {
214    /// New `(1920, 1080)` at `None` scale.
215    fn default() -> Self {
216        (1920, 1080).into()
217    }
218}
219impl_from_and_into_var! {
220    fn from<W: Into<Dip>, H: Into<Dip>>((width, height): (W, H)) -> HeadlessMonitor {
221        HeadlessMonitor::new(DipSize::new(width.into(), height.into()))
222    }
223    fn from<W: Into<Dip>, H: Into<Dip>, F: Into<Factor>>((width, height, scale): (W, H, F)) -> HeadlessMonitor {
224        HeadlessMonitor::new_scaled(DipSize::new(width.into(), height.into()), scale.into())
225    }
226}
227
228/// All information about a monitor that [`MONITORS`] can provide.
229#[derive(Clone)]
230pub struct MonitorInfo {
231    id: MonitorId,
232    is_primary: Var<bool>,
233    name: Var<Txt>,
234    position: Var<PxPoint>,
235    size: Var<PxSize>,
236    video_modes: Var<Vec<VideoMode>>,
237    scale_factor: Var<Factor>,
238    density: Var<PxDensity>,
239    refresh_rate: Var<Frequency>,
240}
241impl fmt::Debug for MonitorInfo {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        f.debug_struct("MonitorInfo")
244            .field("id", &self.id)
245            .field("name", &self.name.get())
246            .field("position", &self.position.get())
247            .field("size", &self.size.get())
248            .field("refresh_rate", &self.refresh_rate.get())
249            .finish_non_exhaustive()
250    }
251}
252impl PartialEq for MonitorInfo {
253    fn eq(&self, other: &Self) -> bool {
254        self.id == other.id && self.name.var_eq(&other.name)
255    }
256}
257impl MonitorInfo {
258    /// New from a [`zng_view_api::MonitorInfo`].
259    fn from_gen(id: MonitorId, info: zng_view_api::window::MonitorInfo) -> Self {
260        MonitorInfo {
261            id,
262            is_primary: var(info.is_primary),
263            name: var(info.name.to_txt()),
264            position: var(info.position),
265            size: var(info.size),
266            scale_factor: var(info.scale_factor),
267            video_modes: var(info.video_modes),
268            refresh_rate: var(info.refresh_rate),
269            density: var(PxDensity::default()),
270        }
271    }
272
273    /// Update variables from fresh [`zng_view_api::MonitorInfo`],
274    /// returns if any value changed.
275    fn update(&self, info: zng_view_api::window::MonitorInfo) -> bool {
276        fn check_set<T: VarValue + PartialEq>(var: &Var<T>, value: T) -> bool {
277            let ne = var.with(|v| v != &value);
278            var.set(value);
279            ne
280        }
281
282        check_set(&self.is_primary, info.is_primary)
283            | check_set(&self.name, info.name.to_txt())
284            | check_set(&self.position, info.position)
285            | check_set(&self.size, info.size)
286            | check_set(&self.scale_factor, info.scale_factor)
287            | check_set(&self.video_modes, info.video_modes)
288            | check_set(&self.refresh_rate, info.refresh_rate)
289    }
290
291    /// Unique ID in this process instance.
292    pub fn id(&self) -> MonitorId {
293        self.id
294    }
295
296    /// If this monitor is the primary screen.
297    pub fn is_primary(&self) -> Var<bool> {
298        self.is_primary.read_only()
299    }
300
301    /// Name of the monitor.
302    pub fn name(&self) -> Var<Txt> {
303        self.name.read_only()
304    }
305    /// Top-left offset of the monitor region in the virtual screen, in pixels.
306    pub fn position(&self) -> Var<PxPoint> {
307        self.position.read_only()
308    }
309    /// Width/height of the monitor region in the virtual screen, in pixels.
310    pub fn size(&self) -> Var<PxSize> {
311        self.size.read_only()
312    }
313
314    /// Exclusive fullscreen video modes.
315    pub fn video_modes(&self) -> Var<Vec<VideoMode>> {
316        self.video_modes.read_only()
317    }
318
319    /// The monitor scale factor.
320    ///
321    /// Can update if the user changes system settings.
322    pub fn scale_factor(&self) -> Var<Factor> {
323        self.scale_factor.read_only()
324    }
325    /// Pixel density config var.
326    pub fn density(&self) -> Var<PxDensity> {
327        self.density.clone()
328    }
329
330    /// The monitor refresh rate.
331    pub fn refresh_rate(&self) -> Var<Frequency> {
332        self.refresh_rate.read_only()
333    }
334
335    /// Gets the monitor area in pixels.
336    pub fn px_rect(&self) -> PxRect {
337        let pos = self.position.get();
338        let size = self.size.get();
339
340        PxRect::new(pos, size)
341    }
342
343    /// Gets the monitor area in device independent pixels.
344    pub fn dip_rect(&self) -> DipRect {
345        let pos = self.position.get();
346        let size = self.size.get();
347        let factor = self.scale_factor.get();
348
349        PxRect::new(pos, size).to_dip(factor)
350    }
351
352    /// Bogus metadata for the [`MonitorId::fallback`].
353    ///
354    /// [`MonitorId::fallback`]: crate::monitor::MonitorId::fallback
355    pub fn fallback() -> Self {
356        let defaults = HeadlessMonitor::default();
357        let fct = 1.fct();
358
359        Self {
360            id: MonitorId::fallback(),
361            is_primary: var(false),
362            name: var("<fallback>".into()),
363            position: var(PxPoint::zero()),
364            size: var(defaults.size.to_px(fct)),
365            video_modes: var(vec![]),
366            scale_factor: var(fct),
367            density: var(PxDensity::default()),
368            refresh_rate: var(60.hertz()),
369        }
370    }
371}
372
373/// A selector that returns a [`MonitorInfo`].
374#[derive(Clone, Default)]
375pub enum MonitorQuery {
376    /// The parent window monitor, or `Primary` if the window has no parent.
377    ///
378    /// Note that the window is not moved automatically if the parent window is moved to another monitor, only
379    /// after the query variable updates.
380    ///
381    /// This is the default value.
382    #[default]
383    ParentOrPrimary,
384
385    /// The primary monitor, if there is any monitor.
386    Primary,
387    /// Custom query closure.
388    ///
389    /// If the closure returns `None` the `ParentOrPrimary` query is used, if there is any.
390    ///
391    /// You can use the [`MONITORS`] service in the query closure to select a monitor.
392    Query(Arc<dyn Fn() -> Option<MonitorInfo> + Send + Sync>),
393}
394impl std::fmt::Debug for MonitorQuery {
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        if f.alternate() {
397            write!(f, "MonitorQuery::")?;
398        }
399        match self {
400            Self::ParentOrPrimary => write!(f, "ParentOrPrimary"),
401            Self::Primary => write!(f, "Primary"),
402            Self::Query(_) => write!(f, "Query(_)"),
403        }
404    }
405}
406impl MonitorQuery {
407    /// New query.
408    pub fn new(query: impl Fn() -> Option<MonitorInfo> + Send + Sync + 'static) -> Self {
409        Self::Query(Arc::new(query))
410    }
411
412    /// Runs the query.
413    pub fn select(&self, window_id: WindowId) -> Option<MonitorInfo> {
414        match self {
415            MonitorQuery::ParentOrPrimary => Self::parent_or_primary_query(window_id),
416            MonitorQuery::Primary => Self::primary_query(),
417            MonitorQuery::Query(q) => q(),
418        }
419    }
420
421    /// Runs the query. Falls back to `Primary`, or the largest or [`MonitorInfo::fallback`].
422    pub fn select_fallback(&self, window_id: WindowId) -> MonitorInfo {
423        match self {
424            MonitorQuery::ParentOrPrimary => Self::parent_or_primary_query(window_id),
425            MonitorQuery::Primary => Self::primary_query(),
426            MonitorQuery::Query(q) => q().or_else(Self::primary_query),
427        }
428        .unwrap_or_else(Self::fallback)
429    }
430
431    fn fallback() -> MonitorInfo {
432        MONITORS_SV.read().monitors.with(|m| {
433            let mut best = None;
434            let mut best_area = Px(0);
435            for m in m.values() {
436                let m_area = m.px_rect().area();
437                if m_area > best_area {
438                    best = Some(m);
439                    best_area = m_area;
440                }
441            }
442            best.cloned().unwrap_or_else(MonitorInfo::fallback)
443        })
444    }
445
446    fn parent_or_primary_query(win_id: WindowId) -> Option<MonitorInfo> {
447        if let Some(parent) = WINDOWS.vars(win_id).unwrap().parent().get()
448            && let Some(w) = WINDOWS.vars(parent)
449        {
450            return if let Some(monitor) = w.actual_monitor().get() {
451                MONITORS.monitor(monitor)
452            } else {
453                w.monitor().get().select(parent)
454            };
455        }
456        MONITORS.primary_monitor().get()
457    }
458
459    fn primary_query() -> Option<MonitorInfo> {
460        MONITORS.primary_monitor().get()
461    }
462}
463impl PartialEq for MonitorQuery {
464    fn eq(&self, other: &Self) -> bool {
465        match (self, other) {
466            (Self::Query(l0), Self::Query(r0)) => Arc::ptr_eq(l0, r0),
467            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
468        }
469    }
470}
471impl_from_and_into_var! {
472    fn from(id: MonitorId) -> MonitorQuery {
473        MonitorQuery::new(move || MONITORS.monitor(id))
474    }
475}
476
477event_args! {
478    /// [`MONITORS_CHANGED_EVENT`] args.
479    pub struct MonitorsChangedArgs {
480        /// Removed monitors.
481        pub removed: Vec<MonitorId>,
482
483        /// Added monitors.
484        ///
485        /// Use the [`MONITORS`] service to get metadata about the added monitors.
486        pub added: Vec<MonitorId>,
487
488        /// Modified monitors.
489        ///
490        /// The monitor metadata is tracked using variables that are now flagged new.
491        pub modified: Vec<MonitorId>,
492
493        ..
494
495        /// Broadcast to all widgets.
496        fn is_in_target(&self, _id: WidgetId) -> bool {
497            true
498        }
499    }
500}
501
502event! {
503    /// Monitors added, removed or modified event.
504    pub static MONITORS_CHANGED_EVENT: MonitorsChangedArgs;
505}