1475 lines
53 KiB
Rust
1475 lines
53 KiB
Rust
// Copyright 2023 System76 <info@system76.com>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
pub mod arrangement;
|
|
// pub mod night_light;
|
|
|
|
use crate::{app, pages};
|
|
use arrangement::Arrangement;
|
|
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
|
|
use cosmic::iced::widget::scrollable::RelativeOffset;
|
|
use cosmic::iced::{Alignment, Length, stream, time};
|
|
use cosmic::widget::{
|
|
self, column, container, dropdown, list_column, segmented_button, tab_bar, text,
|
|
};
|
|
use cosmic::{Apply, Element, Task, surface};
|
|
use cosmic_randr_shell::{
|
|
AdaptiveSyncAvailability, AdaptiveSyncState, List, Output, OutputKey, Transform,
|
|
};
|
|
use cosmic_settings_page::{self as page, Section, section};
|
|
use futures::SinkExt;
|
|
use indexmap::Equivalent;
|
|
use slotmap::{Key, SecondaryMap, SlotMap};
|
|
use std::collections::BTreeMap;
|
|
use std::process::ExitStatus;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, LazyLock};
|
|
use tokio::sync::oneshot;
|
|
|
|
static DPI_SCALES: &[u32] = &[50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300];
|
|
|
|
static DPI_SCALE_LABELS: LazyLock<Vec<String>> =
|
|
LazyLock::new(|| DPI_SCALES.iter().map(|scale| format!("{scale}%")).collect());
|
|
|
|
/// Display color depth options
|
|
#[allow(dead_code)]
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct ColorDepth(usize);
|
|
|
|
// /// Identifies the content to display in the context drawer
|
|
// pub enum ContextDrawer {
|
|
// NightLight,
|
|
// }
|
|
|
|
/// Display mirroring options
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Mirroring {
|
|
Disable,
|
|
// ProjectToAll,
|
|
Project(OutputKey),
|
|
Mirror(OutputKey),
|
|
}
|
|
|
|
/// Night light preferences
|
|
// #[derive(Clone, Copy, Debug)]
|
|
// pub enum NightLight {
|
|
/// Toggles night light's automatic scheduling.
|
|
// AutoSchedule(bool),
|
|
/// Sets the night light schedule.
|
|
// ManualSchedule,
|
|
/// Changes the preferred night light temperature.
|
|
// Temperature(f32),
|
|
/// Toggles night light mode
|
|
// Toggle(bool),
|
|
// }
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Message {
|
|
/// Change placement of display
|
|
Position(OutputKey, i32, i32),
|
|
/// Changes the active display being configured.
|
|
Display(segmented_button::Entity),
|
|
/// Set the color depth of a display.
|
|
ColorDepth(ColorDepth),
|
|
/// Set the color profile of a display.
|
|
ColorProfile(usize),
|
|
/// The dialog was cancelled, and will revert settings.
|
|
DialogCancel,
|
|
/// The dialog was completed.
|
|
DialogComplete,
|
|
/// How long until the dialog automatically cancels, in seconds.
|
|
DialogCountdown,
|
|
/// Toggles display on or off.
|
|
DisplayToggle(bool),
|
|
/// Configures mirroring status of a display.
|
|
Mirroring(Mirroring),
|
|
/// Handle night light preferences.
|
|
// NightLight(NightLight),
|
|
/// Show the night light mode context drawer.
|
|
// NightLightContext,
|
|
/// Set the orientation of a display.
|
|
Orientation(Transform),
|
|
/// Pan the displays view
|
|
Pan(arrangement::Pan),
|
|
/// Status of an applied display change.
|
|
RandrResult(Arc<std::io::Result<ExitStatus>>),
|
|
/// Set the refresh rate of a display.
|
|
RefreshRate(usize),
|
|
/// Set the VRR mode of a display.
|
|
VariableRefreshRate(usize),
|
|
/// Set the resolution of a display.
|
|
Resolution(usize),
|
|
/// Set the preferred scale for a display.
|
|
Scale(usize),
|
|
/// Adjust the display scale.
|
|
AdjustScale(u32),
|
|
/// Refreshes display outputs.
|
|
Update {
|
|
/// Available outputs from cosmic-randr.
|
|
randr: Arc<Result<List, cosmic_randr_shell::Error>>,
|
|
},
|
|
Surface(surface::Action),
|
|
}
|
|
|
|
impl From<Message> for app::Message {
|
|
fn from(message: Message) -> Self {
|
|
let page_message = crate::pages::Message::Displays(message);
|
|
app::Message::PageMessage(page_message)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
enum Randr {
|
|
Mirror(OutputKey),
|
|
Position(i32, i32),
|
|
RefreshRate(u32),
|
|
VariableRefreshRate(AdaptiveSyncState),
|
|
Resolution(u32, u32),
|
|
Scale(u32),
|
|
Transform(Transform),
|
|
Toggle(bool),
|
|
}
|
|
|
|
/// The page struct for the display settings page.
|
|
pub struct Page {
|
|
/// Set when the page is being refreshed
|
|
refreshing_page: Arc<AtomicBool>,
|
|
list: List,
|
|
display_tabs: segmented_button::SingleSelectModel,
|
|
mirror_map: SecondaryMap<OutputKey, OutputKey>,
|
|
mirror_menu: widget::dropdown::multi::Model<String, Mirroring>,
|
|
active_display: OutputKey,
|
|
randr_handle: Option<(oneshot::Sender<()>, cosmic::iced::task::Handle)>,
|
|
hotplug_handle: Option<(oneshot::Sender<()>, cosmic::iced::task::Handle)>,
|
|
config: Config,
|
|
cache: ViewCache,
|
|
// context: Option<ContextDrawer>,
|
|
display_arrangement_scrollable: widget::Id,
|
|
/// Tracks the last pan status.
|
|
last_pan: f32,
|
|
/// The setting to revert to if the next dialog page is cancelled.
|
|
dialog: Option<Randr>,
|
|
/// the instant the setting was changed.
|
|
dialog_countdown: usize,
|
|
show_display_options: bool,
|
|
adjusted_scale: u32,
|
|
}
|
|
|
|
impl Default for Page {
|
|
fn default() -> Self {
|
|
Self {
|
|
refreshing_page: Arc::new(AtomicBool::new(false)),
|
|
list: List::default(),
|
|
display_tabs: segmented_button::SingleSelectModel::default(),
|
|
mirror_map: SecondaryMap::new(),
|
|
mirror_menu: widget::dropdown::multi::model(),
|
|
active_display: OutputKey::default(),
|
|
randr_handle: None,
|
|
hotplug_handle: None,
|
|
config: Config::default(),
|
|
cache: ViewCache::default(),
|
|
// context: None,
|
|
display_arrangement_scrollable: widget::Id::unique(),
|
|
last_pan: 0.5,
|
|
dialog: None,
|
|
dialog_countdown: 0,
|
|
show_display_options: true,
|
|
adjusted_scale: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Config {
|
|
/// Whether night light is enabled.
|
|
// night_light_enabled: bool,
|
|
refresh_rate: Option<u32>,
|
|
vrr: Option<AdaptiveSyncState>,
|
|
resolution: Option<(u32, u32)>,
|
|
scale: u32,
|
|
}
|
|
|
|
/// Cached view content for widgets.
|
|
#[derive(Default)]
|
|
struct ViewCache {
|
|
modes: BTreeMap<(u32, u32), Vec<u32>>,
|
|
orientations: [String; 4],
|
|
vrr_modes: Vec<String>,
|
|
refresh_rates: Vec<String>,
|
|
resolutions: Vec<String>,
|
|
orientation_selected: Option<usize>,
|
|
refresh_rate_selected: Option<usize>,
|
|
vrr_selected: Option<usize>,
|
|
resolution_selected: Option<usize>,
|
|
scale_selected: Option<usize>,
|
|
}
|
|
|
|
impl page::AutoBind<crate::pages::Message> for Page {}
|
|
|
|
impl page::Page<crate::pages::Message> for Page {
|
|
fn content(
|
|
&self,
|
|
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
|
) -> Option<page::Content> {
|
|
Some(vec![
|
|
// Night light
|
|
// sections.insert(
|
|
// Section::default()
|
|
// .descriptions(vec![
|
|
// text::NIGHT_LIGHT.as_str().into(),
|
|
// text::NIGHT_LIGHT_AUTO.as_str().into(),
|
|
// text::NIGHT_LIGHT_DESCRIPTION.as_str().into(),
|
|
// ])
|
|
// .view::<Page>(move |_binder, page, _section| page.night_light_view()),
|
|
// ),
|
|
// Display arrangement
|
|
sections.insert(display_arrangement()),
|
|
// Display configuration
|
|
sections.insert(display_configuration()),
|
|
])
|
|
}
|
|
|
|
fn info(&self) -> page::Info {
|
|
page::Info::new("display", "preferences-desktop-display-symbolic")
|
|
.title(fl!("display"))
|
|
.description(fl!("xdg-entry-displays-comment"))
|
|
}
|
|
|
|
#[cfg(not(feature = "test"))]
|
|
fn on_enter(&mut self) -> Task<crate::pages::Message> {
|
|
use std::time::Duration;
|
|
|
|
self.cache.orientations = [
|
|
fl!("orientation", "standard"),
|
|
fl!("orientation", "rotate-90"),
|
|
fl!("orientation", "rotate-180"),
|
|
fl!("orientation", "rotate-270"),
|
|
];
|
|
|
|
let mut tasks = Vec::with_capacity(3);
|
|
tasks.push(cosmic::task::future(on_enter()));
|
|
|
|
if let Some((canceller, handle)) = self.randr_handle.take() {
|
|
_ = canceller.send(());
|
|
handle.abort();
|
|
}
|
|
|
|
if let Some((canceller, handle)) = self.hotplug_handle.take() {
|
|
_ = canceller.send(());
|
|
handle.abort();
|
|
}
|
|
|
|
self.refreshing_page.store(true, Ordering::SeqCst);
|
|
|
|
#[cfg(feature = "wayland")]
|
|
{
|
|
let refreshing_page = self.refreshing_page.clone();
|
|
let (tx, rx) = cosmic_randr::channel();
|
|
let (canceller, cancelled) = oneshot::channel::<()>();
|
|
let runtime = tokio::runtime::Handle::current();
|
|
|
|
// Spawns a background service to monitor for display state changes.
|
|
// This must be spawned onto its own thread because `*mut wayland_sys::client::wl_display` is not Send-able.
|
|
tokio::task::spawn_blocking(move || {
|
|
let dispatcher = std::pin::pin!(async move {
|
|
let Ok((mut context, mut event_queue)) = cosmic_randr::connect(tx) else {
|
|
return;
|
|
};
|
|
|
|
loop {
|
|
if context.dispatch(&mut event_queue).await.is_err() {
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
runtime.block_on(futures::future::select(cancelled, dispatcher));
|
|
});
|
|
|
|
// Forward messages from another thread to prevent the monitoring thread from blocking.
|
|
let (randr_task, randr_handle) = Task::stream(stream::channel(
|
|
1,
|
|
|mut emitter: futures::channel::mpsc::Sender<_>| async move {
|
|
while let Some(message) = rx.recv().await {
|
|
if let cosmic_randr::Message::ManagerDone = message
|
|
&& !refreshing_page.swap(true, Ordering::SeqCst)
|
|
{
|
|
_ = emitter.send(on_enter().await).await;
|
|
}
|
|
}
|
|
},
|
|
))
|
|
.abortable();
|
|
|
|
tasks.push(randr_task);
|
|
self.randr_handle = Some((canceller, randr_handle));
|
|
}
|
|
|
|
// Channels for communicating messages from the DRM hotplug thread.
|
|
let (hotplug_cancel_tx, hotplug_cancel_rx) = oneshot::channel::<()>();
|
|
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
|
|
|
// Spawn a background thread for asynchronously polling a udev monitor for DRM
|
|
// hotplug events. As udev is not thread-safe, it needs its own dedicated thread.
|
|
let tokio_handle = tokio::runtime::Handle::current();
|
|
std::thread::spawn(move || {
|
|
tokio_handle.block_on(async move {
|
|
let Ok(builder) = udev::MonitorBuilder::new() else {
|
|
return;
|
|
};
|
|
|
|
let Ok(builder) = builder.match_subsystem("drm") else {
|
|
return;
|
|
};
|
|
|
|
let Ok(socket) = builder.listen() else {
|
|
return;
|
|
};
|
|
|
|
let Ok(mut async_fd) = tokio::io::unix::AsyncFd::new(socket) else {
|
|
return;
|
|
};
|
|
|
|
let emitter = std::pin::pin!(async move {
|
|
loop {
|
|
// If any DRM events occur, a message updating the display list will be sent.
|
|
if let Ok(mut guard) = async_fd.writable().await {
|
|
guard.clear_ready();
|
|
|
|
let drm_hotplug_occurred = &mut false;
|
|
async_fd.get_mut().iter().for_each(|_| {
|
|
*drm_hotplug_occurred = true;
|
|
});
|
|
|
|
if *drm_hotplug_occurred {
|
|
_ = tx.send(on_enter().await).await;
|
|
}
|
|
}
|
|
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
}
|
|
});
|
|
|
|
let cancellation = std::pin::pin!(async move {
|
|
_ = hotplug_cancel_rx.await;
|
|
});
|
|
|
|
futures::future::select(cancellation, emitter).await;
|
|
});
|
|
});
|
|
|
|
// Forward messages from the DRM hotplug thread.
|
|
let (hotplug_task, hotplug_handle) = Task::stream(stream::channel(
|
|
1,
|
|
|mut emitter: futures::channel::mpsc::Sender<pages::Message>| async move {
|
|
while let Some(message) = rx.recv().await {
|
|
_ = emitter.send(message).await;
|
|
}
|
|
},
|
|
))
|
|
.abortable();
|
|
|
|
tasks.push(hotplug_task);
|
|
self.hotplug_handle = Some((hotplug_cancel_tx, hotplug_handle));
|
|
|
|
cosmic::task::batch(tasks)
|
|
}
|
|
|
|
fn on_leave(&mut self) -> Task<crate::pages::Message> {
|
|
if let Some((canceller, handle)) = self.hotplug_handle.take() {
|
|
_ = canceller.send(());
|
|
handle.abort();
|
|
}
|
|
|
|
if let Some((canceller, handle)) = self.randr_handle.take() {
|
|
_ = canceller.send(());
|
|
handle.abort();
|
|
}
|
|
|
|
Task::none()
|
|
}
|
|
|
|
#[cfg(feature = "test")]
|
|
fn on_enter(&mut self) -> Task<crate::pages::Message> {
|
|
cosmic::task::future(async move {
|
|
let mut randr = List::default();
|
|
|
|
let test_mode = randr.modes.insert(cosmic_randr_shell::Mode {
|
|
size: (1920, 1080),
|
|
refresh_rate: 144_000,
|
|
preferred: true,
|
|
});
|
|
|
|
randr.outputs.insert(cosmic_randr_shell::Output {
|
|
name: "Dummy-1".into(),
|
|
enabled: true,
|
|
make: None,
|
|
model: "Test 1".into(),
|
|
serial_number: "Serial 1".into(),
|
|
mirroring: None,
|
|
physical: (1, 1),
|
|
position: (0, 0),
|
|
scale: 1.0,
|
|
transform: Some(Transform::Normal),
|
|
modes: vec![test_mode],
|
|
current: Some(test_mode),
|
|
adaptive_sync: None,
|
|
adaptive_sync_availability: None,
|
|
xwayland_primary: None,
|
|
});
|
|
|
|
randr.outputs.insert(cosmic_randr_shell::Output {
|
|
name: "Dummy-2".into(),
|
|
enabled: true,
|
|
make: None,
|
|
model: "Test 1".into(),
|
|
serial_number: "Serial 2".into(),
|
|
mirroring: None,
|
|
physical: (1, 1),
|
|
position: (1920, 0),
|
|
scale: 1.0,
|
|
transform: Some(Transform::Normal),
|
|
modes: vec![test_mode],
|
|
current: Some(test_mode),
|
|
adaptive_sync: Some(AdaptiveSyncState::Disabled),
|
|
adaptive_sync_availability: Some(AdaptiveSyncAvailability::Supported),
|
|
xwayland_primary: None,
|
|
});
|
|
|
|
crate::pages::Message::Displays(Message::Update {
|
|
randr: Arc::new(Ok(randr)),
|
|
})
|
|
})
|
|
}
|
|
|
|
// fn context_drawer(&self) -> Option<Element<pages::Message>> {
|
|
// Some(match self.context {
|
|
|
|
// Some(ContextDrawer::NightLight) => self.night_light_context_view(),
|
|
|
|
// None => return None,
|
|
// })
|
|
// }
|
|
|
|
/// Opens a dialog to confirm the display settings.
|
|
///
|
|
/// This dialog has a 10 (arbitrary) second counter which will
|
|
/// automatically revert to the original display settings when depleted.
|
|
///
|
|
/// To make a setting activate this dialog. Call the `set_dialog` method with
|
|
/// the Randr enum value which undos the current change. Makde sure the
|
|
/// return value is returned with the `exec_value` return value within a batch
|
|
/// Task.
|
|
fn dialog(&self) -> Option<Element<'_, pages::Message>> {
|
|
self.dialog?;
|
|
let element = widget::dialog()
|
|
.title(fl!("dialog", "title"))
|
|
.body(fl!("dialog", "change-prompt", time = self.dialog_countdown))
|
|
.primary_action(
|
|
widget::button::suggested(fl!("dialog", "keep-changes"))
|
|
.on_press(pages::Message::Displays(Message::DialogComplete)),
|
|
)
|
|
.secondary_action(
|
|
widget::button::standard(fl!("dialog", "revert-settings"))
|
|
.on_press(pages::Message::Displays(Message::DialogCancel)),
|
|
)
|
|
.into();
|
|
Some(element)
|
|
}
|
|
}
|
|
|
|
impl Page {
|
|
pub fn update(&mut self, message: Message) -> Task<app::Message> {
|
|
match message {
|
|
Message::RandrResult(result) => {
|
|
if let Some(Err(why)) = Arc::into_inner(result) {
|
|
tracing::error!(why = why.to_string(), "cosmic-randr error");
|
|
// Cancel the revert dialog if resolution or refresh rate did not change.
|
|
// RandR may revert those changes in certain circumstances so showing the
|
|
// dialog is superfluous and confusing.
|
|
if let Some(mode) =
|
|
self.list
|
|
.outputs
|
|
.get(self.active_display)
|
|
.and_then(|display| {
|
|
display.current.and_then(|key| self.list.modes.get(key))
|
|
})
|
|
{
|
|
tracing::debug!(old = ?self.dialog, new = ?mode, "Mode update");
|
|
match self.dialog {
|
|
Some(Randr::Resolution(width, height)) => {
|
|
if mode.size.0 == width && mode.size.1 == height {
|
|
self.config.resolution = Some((width, height));
|
|
return self.update(Message::DialogComplete);
|
|
}
|
|
}
|
|
Some(Randr::RefreshRate(rate)) => {
|
|
if mode.refresh_rate == rate {
|
|
self.config.refresh_rate = Some(rate);
|
|
return self.update(Message::DialogComplete);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Message::DialogCancel => {
|
|
let Some(request) = self.dialog else {
|
|
return Task::none();
|
|
};
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
self.dialog = None;
|
|
self.dialog_countdown = 0;
|
|
return self.exec_randr(output, request);
|
|
}
|
|
|
|
Message::DialogComplete => {
|
|
self.dialog = None;
|
|
self.dialog_countdown = 0;
|
|
}
|
|
|
|
Message::DialogCountdown => {
|
|
if self.dialog_countdown == 0 {
|
|
if self.dialog.is_some() {
|
|
return cosmic::task::message(app::Message::from(Message::DialogCancel));
|
|
}
|
|
} else {
|
|
self.dialog_countdown -= 1;
|
|
return cosmic::task::future(async move {
|
|
tokio::time::sleep(time::Duration::from_secs(1)).await;
|
|
Message::DialogCountdown
|
|
});
|
|
}
|
|
}
|
|
|
|
Message::Display(display) => self.set_display(display),
|
|
|
|
Message::ColorDepth(color_depth) => return self.set_color_depth(color_depth),
|
|
|
|
Message::ColorProfile(profile) => return self.set_color_profile(profile),
|
|
|
|
Message::DisplayToggle(enable) => return self.toggle_display(enable),
|
|
|
|
Message::Mirroring(mirroring) => match mirroring {
|
|
Mirroring::Disable => {
|
|
for k in self.mirror_map.keys() {
|
|
if k.equivalent(&self.active_display) {
|
|
return self.toggle_display(true);
|
|
}
|
|
|
|
if let Some(v) = self.mirror_map.get(k)
|
|
&& v.equivalent(&self.active_display)
|
|
{
|
|
if let Some(output) = self.list.outputs.get(k) {
|
|
return self.exec_randr(output, Randr::Toggle(true));
|
|
} else {
|
|
return Task::none();
|
|
}
|
|
}
|
|
}
|
|
|
|
return Task::none();
|
|
}
|
|
|
|
Mirroring::Mirror(from_display) => {
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
return self.exec_randr(output, Randr::Mirror(from_display));
|
|
}
|
|
|
|
Mirroring::Project(to_display) => {
|
|
let Some(output) = self.list.outputs.get(to_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
return self.exec_randr(output, Randr::Mirror(self.active_display));
|
|
} // Mirroring::ProjectToAll => (),
|
|
},
|
|
|
|
// Message::NightLight(night_light) => {}
|
|
//
|
|
// Message::NightLightContext => {
|
|
// self.context = Some(ContextDrawer::NightLight);
|
|
// return cosmic::task::message(app::Message::OpenContextDrawer(
|
|
// text::NIGHT_LIGHT.clone().into(),
|
|
// ));
|
|
// }
|
|
Message::Orientation(orientation) => return self.set_orientation(orientation),
|
|
|
|
Message::Pan(pan) => {
|
|
match pan {
|
|
arrangement::Pan::Left => self.last_pan = 0.0f32.max(self.last_pan - 0.01),
|
|
arrangement::Pan::Right => self.last_pan = 1.0f32.min(self.last_pan + 0.01),
|
|
}
|
|
|
|
return cosmic::iced::widget::scrollable::snap_to(
|
|
self.display_arrangement_scrollable.clone(),
|
|
RelativeOffset {
|
|
x: Some(self.last_pan),
|
|
y: None,
|
|
},
|
|
);
|
|
}
|
|
|
|
Message::Position(display, x, y) => return self.set_position(display, x, y),
|
|
|
|
Message::RefreshRate(rate) => return self.set_refresh_rate(rate),
|
|
|
|
Message::VariableRefreshRate(mode) => return self.set_vrr(mode),
|
|
|
|
Message::Resolution(option) => return self.set_resolution(option),
|
|
|
|
Message::Scale(option) => {
|
|
self.adjusted_scale = 0;
|
|
return self.set_scale(option);
|
|
}
|
|
|
|
Message::AdjustScale(scale) => {
|
|
if self.adjusted_scale != scale {
|
|
self.adjusted_scale = scale;
|
|
|
|
if let Some(option) = self.cache.scale_selected {
|
|
return self.set_scale(option);
|
|
}
|
|
}
|
|
}
|
|
|
|
Message::Update { randr } => {
|
|
match Arc::into_inner(randr) {
|
|
Some(Ok(outputs)) => {
|
|
self.update_displays(outputs);
|
|
}
|
|
|
|
Some(Err(why)) => {
|
|
tracing::error!(why = why.to_string(), "error fetching displays");
|
|
}
|
|
|
|
None => (),
|
|
}
|
|
|
|
self.refreshing_page.store(false, Ordering::SeqCst);
|
|
}
|
|
|
|
Message::Surface(a) => {
|
|
return cosmic::task::message(crate::app::Message::Surface(a));
|
|
}
|
|
}
|
|
|
|
self.last_pan = 0.5;
|
|
cosmic::iced::widget::scrollable::snap_to(
|
|
self.display_arrangement_scrollable.clone(),
|
|
RelativeOffset {
|
|
x: Some(0.5),
|
|
y: Some(0.5),
|
|
},
|
|
)
|
|
}
|
|
|
|
// /// Displays the night light context drawer.
|
|
// pub fn night_light_context_view(&self) -> Element<pages::Message> {
|
|
// column().into()
|
|
// }
|
|
|
|
/// Reloads the display list, and all information relevant to the active display.
|
|
pub fn update_displays(&mut self, list: List) {
|
|
let active_display_name = self
|
|
.display_tabs
|
|
.text_remove(self.display_tabs.active())
|
|
.unwrap_or_default();
|
|
let mut active_tab_pos: u16 = 0;
|
|
|
|
self.active_display = OutputKey::null();
|
|
self.display_tabs = Default::default();
|
|
self.mirror_map = SecondaryMap::new();
|
|
self.list = list;
|
|
|
|
let sorted_outputs = self
|
|
.list
|
|
.outputs
|
|
.iter()
|
|
.map(|(key, output)| (&*output.name, key))
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
for (pos, (_name, id)) in sorted_outputs.into_iter().enumerate() {
|
|
let Some(output) = self.list.outputs.get(id) else {
|
|
continue;
|
|
};
|
|
|
|
if let Some(mirroring_from) = output.mirroring.as_deref() {
|
|
for (other_id, other_output) in &self.list.outputs {
|
|
if other_output.name == mirroring_from {
|
|
self.mirror_map.insert(id, other_id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let text = crate::utils::display_name(&output.name, output.physical);
|
|
|
|
if text == active_display_name {
|
|
active_tab_pos = pos as u16;
|
|
}
|
|
|
|
self.display_tabs.insert().text(text).data::<OutputKey>(id);
|
|
}
|
|
|
|
self.display_tabs.activate_position(active_tab_pos);
|
|
|
|
// Retrieve data for the first, activated display.
|
|
self.set_display(self.display_tabs.active());
|
|
}
|
|
|
|
/// Sets the dialog to be shown to the user. Will not show a dialog if the
|
|
/// current request does not change anything.
|
|
fn set_dialog(&mut self, revert_request: Randr, current_request: &Randr) -> Task<app::Message> {
|
|
if revert_request == *current_request {
|
|
return Task::none();
|
|
}
|
|
self.dialog = Some(revert_request);
|
|
self.dialog_countdown = 10;
|
|
cosmic::task::future(async {
|
|
tokio::time::sleep(time::Duration::from_secs(1)).await;
|
|
app::Message::from(Message::DialogCountdown)
|
|
})
|
|
}
|
|
|
|
/// Changes the color depth of the active display.
|
|
pub fn set_color_depth(&mut self, _depth: ColorDepth) -> Task<app::Message> {
|
|
unimplemented!()
|
|
}
|
|
|
|
/// Changes the color profile of the active display.
|
|
pub fn set_color_profile(&mut self, _profile: usize) -> Task<app::Message> {
|
|
unimplemented!()
|
|
}
|
|
|
|
/// Changes the active display, and regenerates available options for it.
|
|
pub fn set_display(&mut self, display: segmented_button::Entity) {
|
|
let Some(&output_id) = self.display_tabs.data::<OutputKey>(display) else {
|
|
return;
|
|
};
|
|
|
|
let Some(output) = self.list.outputs.get_mut(output_id) else {
|
|
return;
|
|
};
|
|
|
|
self.display_tabs.activate(display);
|
|
self.active_display = output_id;
|
|
self.config.refresh_rate = None;
|
|
self.config.resolution = None;
|
|
self.config.vrr = output.adaptive_sync;
|
|
self.config.scale = (output.scale * 100.0) as u32;
|
|
|
|
self.cache.modes.clear();
|
|
self.cache.refresh_rates.clear();
|
|
self.cache.vrr_modes.clear();
|
|
self.cache.resolutions.clear();
|
|
self.cache.orientation_selected = match output.transform {
|
|
Some(Transform::Normal) => Some(0),
|
|
Some(Transform::Rotate90) => Some(1),
|
|
Some(Transform::Rotate180) => Some(2),
|
|
Some(Transform::Rotate270) => Some(3),
|
|
_ => None,
|
|
};
|
|
self.cache.resolution_selected = None;
|
|
self.cache.refresh_rate_selected = None;
|
|
self.cache.vrr_selected = None;
|
|
|
|
let selected_scale = DPI_SCALES
|
|
.iter()
|
|
.position(|scale| self.config.scale <= *scale)
|
|
.unwrap_or(DPI_SCALES.len() - 1);
|
|
|
|
self.adjusted_scale = ((self.config.scale % 25).min(20) as f32 / 5.0).round() as u32 * 5;
|
|
self.cache.scale_selected = Some(if self.adjusted_scale != 0 && selected_scale > 0 {
|
|
selected_scale - 1
|
|
} else {
|
|
selected_scale
|
|
});
|
|
|
|
if let Some(current_mode_id) = output.current {
|
|
for (mode_id, mode) in output
|
|
.modes
|
|
.iter()
|
|
.filter_map(|&id| self.list.modes.get(id).map(|m| (id, m)))
|
|
{
|
|
let refresh_rates = self.cache.modes.entry(mode.size).or_default();
|
|
|
|
refresh_rates.push(mode.refresh_rate);
|
|
|
|
if current_mode_id == mode_id {
|
|
self.cache.refresh_rate_selected = Some(refresh_rates.len() - 1);
|
|
self.cache.resolution_selected = Some(self.cache.modes.len() - 1);
|
|
self.config.resolution = Some(mode.size);
|
|
self.config.refresh_rate = Some(mode.refresh_rate);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (&resolution, rates) in self.cache.modes.iter().rev() {
|
|
self.cache
|
|
.resolutions
|
|
.push(format!("{}x{}", resolution.0, resolution.1));
|
|
if Some(resolution) == self.config.resolution {
|
|
cache_rates(&mut self.cache.refresh_rates, rates);
|
|
}
|
|
}
|
|
|
|
if let Some(state) = output.adaptive_sync {
|
|
match output.adaptive_sync_availability {
|
|
Some(AdaptiveSyncAvailability::Supported) => {
|
|
self.cache.vrr_modes = vec![
|
|
fl!("vrr", "force"),
|
|
fl!("vrr", "auto"),
|
|
fl!("vrr", "disabled"),
|
|
];
|
|
self.cache.vrr_selected = match state {
|
|
AdaptiveSyncState::Always => Some(0),
|
|
AdaptiveSyncState::Auto => Some(1),
|
|
AdaptiveSyncState::Disabled => Some(2),
|
|
};
|
|
}
|
|
Some(AdaptiveSyncAvailability::RequiresModeset) => {
|
|
self.cache.vrr_modes = vec![fl!("vrr", "enabled"), fl!("vrr", "disabled")];
|
|
self.cache.vrr_selected = match state {
|
|
AdaptiveSyncState::Always | AdaptiveSyncState::Auto => Some(0),
|
|
AdaptiveSyncState::Disabled => Some(1),
|
|
};
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
self.mirror_menu.clear();
|
|
|
|
self.mirror_menu.insert(widget::dropdown::multi::list(
|
|
None,
|
|
vec![(fl!("mirroring", "dont"), Mirroring::Disable)],
|
|
));
|
|
|
|
self.mirror_menu.insert(widget::dropdown::multi::list(
|
|
None,
|
|
self.list
|
|
.outputs
|
|
.iter()
|
|
.filter(|&(other_id, _)| other_id != output_id)
|
|
.map(|(other_id, other_output)| {
|
|
(
|
|
fl!("mirroring", "project", display = other_output.name.as_str()),
|
|
Mirroring::Project(other_id),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
));
|
|
|
|
self.mirror_menu.insert(widget::dropdown::multi::list(
|
|
None,
|
|
self.list
|
|
.outputs
|
|
.iter()
|
|
.filter(|&(other_id, _)| other_id != output_id)
|
|
.map(|(other_id, other_output)| {
|
|
(
|
|
fl!("mirroring", "mirror", display = other_output.name.as_str()),
|
|
Mirroring::Mirror(other_id),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
));
|
|
|
|
self.mirror_menu.selected = self
|
|
.mirror_map
|
|
.get(output_id)
|
|
.map(|id| Mirroring::Mirror(*id));
|
|
|
|
self.show_display_options = self.mirror_menu.selected.is_none();
|
|
|
|
if self.mirror_menu.selected.is_none() {
|
|
self.mirror_menu.selected = self
|
|
.mirror_map
|
|
.iter()
|
|
.find(|&(_, mirrored_id)| *mirrored_id == output_id)
|
|
.map(|(projected_id, _)| Mirroring::Project(projected_id));
|
|
}
|
|
|
|
if self.mirror_menu.selected.is_none() {
|
|
// If mirror menu is not set, set it to don't mirror.
|
|
self.mirror_menu.selected = Some(Mirroring::Disable);
|
|
}
|
|
}
|
|
|
|
/// Change display orientation.
|
|
pub fn set_orientation(&mut self, transform: Transform) -> Task<app::Message> {
|
|
let request = Randr::Transform(transform);
|
|
|
|
let mut tasks = Vec::with_capacity(2);
|
|
tasks.push(match self.cache.orientation_selected {
|
|
Some(orientation) => self.set_dialog(
|
|
Randr::Transform(match orientation {
|
|
1 => Transform::Rotate90,
|
|
2 => Transform::Flipped180,
|
|
3 => Transform::Flipped270,
|
|
_ => Transform::Normal,
|
|
}),
|
|
&request,
|
|
),
|
|
None => Task::none(),
|
|
});
|
|
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
self.cache.orientation_selected = match transform {
|
|
Transform::Normal => Some(0),
|
|
Transform::Rotate90 => Some(1),
|
|
Transform::Rotate180 => Some(2),
|
|
_ => Some(3),
|
|
};
|
|
|
|
tasks.push(self.exec_randr(output, Randr::Transform(transform)));
|
|
|
|
Task::batch(tasks)
|
|
}
|
|
|
|
/// Changes the position of the display.
|
|
pub fn set_position(&mut self, display: OutputKey, x: i32, y: i32) -> Task<app::Message> {
|
|
let Some(output) = self.list.outputs.get_mut(display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
output.position = (x, y);
|
|
|
|
if cfg!(feature = "test") {
|
|
tracing::debug!("set position {x},{y}");
|
|
return Task::none();
|
|
}
|
|
|
|
let output = &self.list.outputs[display];
|
|
self.exec_randr(output, Randr::Position(x, y))
|
|
}
|
|
|
|
/// Changes the refresh rate of the active display.
|
|
pub fn set_refresh_rate(&mut self, option: usize) -> Task<app::Message> {
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
if let Some(ref resolution) = self.config.resolution
|
|
&& let Some(rates) = self.cache.modes.get(resolution)
|
|
&& let Some(&rate) = rates.get(option)
|
|
{
|
|
self.cache.refresh_rate_selected = Some(option);
|
|
self.config.refresh_rate = Some(rate);
|
|
return self.exec_randr(output, Randr::RefreshRate(rate));
|
|
}
|
|
|
|
Task::none()
|
|
}
|
|
|
|
/// Changes the variable refresh rate mode of the active display.
|
|
pub fn set_vrr(&mut self, option: usize) -> Task<app::Message> {
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
let mode = match output.adaptive_sync_availability {
|
|
Some(AdaptiveSyncAvailability::Supported) => match option {
|
|
0 => AdaptiveSyncState::Always,
|
|
1 => AdaptiveSyncState::Auto,
|
|
2 => AdaptiveSyncState::Disabled,
|
|
_ => return Task::none(),
|
|
},
|
|
Some(AdaptiveSyncAvailability::RequiresModeset) => match option {
|
|
0 => AdaptiveSyncState::Always,
|
|
1 => AdaptiveSyncState::Disabled,
|
|
_ => return Task::none(),
|
|
},
|
|
_ => return Task::none(),
|
|
};
|
|
|
|
self.cache.vrr_selected = Some(option);
|
|
self.config.vrr = Some(mode);
|
|
self.exec_randr(output, Randr::VariableRefreshRate(mode))
|
|
}
|
|
|
|
/// Change the resolution of the active display.
|
|
pub fn set_resolution(&mut self, option: usize) -> Task<app::Message> {
|
|
let mut tasks = Vec::with_capacity(2);
|
|
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
let Some((&resolution, rates)) = self.cache.modes.iter().rev().nth(option) else {
|
|
return Task::none();
|
|
};
|
|
|
|
self.cache.refresh_rates.clear();
|
|
cache_rates(&mut self.cache.refresh_rates, rates);
|
|
|
|
let Some(&rate) = rates.first() else {
|
|
return Task::none();
|
|
};
|
|
|
|
let request = Randr::Resolution(resolution.0, resolution.1);
|
|
let mut revert_request = request;
|
|
if let Some(resolution) = self.config.resolution {
|
|
revert_request = Randr::Resolution(resolution.0, resolution.1);
|
|
}
|
|
|
|
self.config.refresh_rate = Some(rate);
|
|
self.config.resolution = Some(resolution);
|
|
self.cache.refresh_rate_selected = Some(0);
|
|
self.cache.resolution_selected = Some(option);
|
|
tasks.push(self.exec_randr(output, Randr::Resolution(resolution.0, resolution.1)));
|
|
tasks.push(self.set_dialog(revert_request, &request));
|
|
|
|
Task::batch(tasks)
|
|
}
|
|
|
|
/// Set the scale of the active display.
|
|
pub fn set_scale(&mut self, option: usize) -> Task<app::Message> {
|
|
let Some(output) = self.list.outputs.get(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
let mut tasks = Vec::with_capacity(2);
|
|
|
|
let scale = (option * 25 + 50) as u32 + self.adjusted_scale.min(20);
|
|
|
|
self.cache.scale_selected = Some(option);
|
|
self.config.scale = scale;
|
|
tasks.push(self.exec_randr(output, Randr::Scale(scale)));
|
|
Task::batch(tasks)
|
|
}
|
|
|
|
/// Enables or disables the active display.
|
|
pub fn toggle_display(&mut self, enable: bool) -> Task<app::Message> {
|
|
let mut tasks = Vec::with_capacity(2);
|
|
let request = Randr::Toggle(enable);
|
|
|
|
let Some(output) = self.list.outputs.get_mut(self.active_display) else {
|
|
return Task::none();
|
|
};
|
|
|
|
let revert_request = Randr::Toggle(output.enabled);
|
|
let current_request = request;
|
|
|
|
output.enabled = enable;
|
|
|
|
let output = &self.list.outputs[self.active_display];
|
|
tasks.push(self.exec_randr(output, request));
|
|
tasks.push(self.set_dialog(revert_request, ¤t_request));
|
|
Task::batch(tasks)
|
|
}
|
|
|
|
/// Applies a display configuration via `cosmic-randr`.
|
|
fn exec_randr(&self, output: &Output, request: Randr) -> Task<app::Message> {
|
|
let mut tasks = Vec::with_capacity(2);
|
|
|
|
// Removes the dialog if no change is being made
|
|
if Some(request) == self.dialog {
|
|
tasks.push(cosmic::task::message(app::Message::from(
|
|
Message::DialogComplete,
|
|
)));
|
|
}
|
|
|
|
let name = &*output.name;
|
|
let mut task = tokio::process::Command::new("cosmic-randr");
|
|
|
|
match request {
|
|
Randr::Mirror(from_id) => {
|
|
let Some(from_output) = self.list.outputs.get(from_id) else {
|
|
return Task::none();
|
|
};
|
|
|
|
task.arg("mirror").arg(&output.name).arg(&from_output.name);
|
|
}
|
|
|
|
Randr::Position(x, y) => {
|
|
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
|
|
return Task::none();
|
|
};
|
|
|
|
task.arg("mode")
|
|
.arg("--pos-x")
|
|
.arg(itoa::Buffer::new().format(x))
|
|
.arg("--pos-y")
|
|
.arg(itoa::Buffer::new().format(y))
|
|
.arg(name)
|
|
.arg(itoa::Buffer::new().format(current.size.0))
|
|
.arg(itoa::Buffer::new().format(current.size.1));
|
|
}
|
|
|
|
Randr::RefreshRate(rate) => {
|
|
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
|
|
return Task::none();
|
|
};
|
|
|
|
task.arg("mode")
|
|
.arg("--refresh")
|
|
.arg(format!("{}.{:03}", rate / 1000, rate % 1000))
|
|
.arg(name)
|
|
.arg(itoa::Buffer::new().format(current.size.0))
|
|
.arg(itoa::Buffer::new().format(current.size.1));
|
|
}
|
|
|
|
Randr::VariableRefreshRate(mode) => {
|
|
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
|
|
return Task::none();
|
|
};
|
|
|
|
task.arg("mode")
|
|
.arg("--adaptive-sync")
|
|
.arg(<&'static str>::from(mode))
|
|
.arg(name)
|
|
.arg(itoa::Buffer::new().format(current.size.0))
|
|
.arg(itoa::Buffer::new().format(current.size.1));
|
|
}
|
|
|
|
Randr::Resolution(width, height) => {
|
|
task.arg("mode")
|
|
.arg(name)
|
|
.arg(itoa::Buffer::new().format(width))
|
|
.arg(itoa::Buffer::new().format(height));
|
|
}
|
|
|
|
Randr::Scale(scale) => {
|
|
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
|
|
return Task::none();
|
|
};
|
|
|
|
let rate = current.refresh_rate;
|
|
|
|
task.arg("mode")
|
|
.arg("--scale")
|
|
.arg(format!("{}.{:02}", scale / 100, scale % 100))
|
|
.arg("--refresh")
|
|
.arg(format!("{}.{:03}", rate / 1000, rate % 1000))
|
|
.arg(name)
|
|
.arg(itoa::Buffer::new().format(current.size.0))
|
|
.arg(itoa::Buffer::new().format(current.size.1));
|
|
}
|
|
|
|
Randr::Toggle(enable) => {
|
|
task.arg(if enable { "enable" } else { "disable" })
|
|
.arg(name);
|
|
}
|
|
|
|
Randr::Transform(transform) => {
|
|
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
|
|
return Task::none();
|
|
};
|
|
|
|
task.arg("mode")
|
|
.arg("--transform")
|
|
.arg(&*format!("{transform}"))
|
|
.arg(name)
|
|
.arg(itoa::Buffer::new().format(current.size.0))
|
|
.arg(itoa::Buffer::new().format(current.size.1));
|
|
}
|
|
}
|
|
|
|
tasks.push(cosmic::task::future(async move {
|
|
tracing::debug!(?task, "executing");
|
|
app::Message::from(Message::RandrResult(Arc::new(task.status().await)))
|
|
}));
|
|
Task::batch(tasks)
|
|
}
|
|
}
|
|
|
|
/// View for the display arrangement section.
|
|
pub fn display_arrangement() -> Section<crate::pages::Message> {
|
|
crate::slab!(descriptions {
|
|
_display_arrangement = fl!("display", "arrangement");
|
|
display_arrangement_desc = fl!("display", "arrangement-desc");
|
|
});
|
|
|
|
Section::default()
|
|
.title(fl!("display", "arrangement"))
|
|
.descriptions(descriptions)
|
|
// Show section when there is more than 1 display
|
|
.show_while::<Page>(|page| page.list.outputs.len() > 1)
|
|
.view::<Page>(move |_binder, page, section| {
|
|
let descriptions = §ion.descriptions;
|
|
let cosmic::cosmic_theme::Spacing {
|
|
space_xxs, space_m, ..
|
|
} = cosmic::theme::spacing();
|
|
|
|
column::with_capacity(2)
|
|
.push(
|
|
text::body(&descriptions[display_arrangement_desc])
|
|
.apply(container)
|
|
.padding([space_xxs, space_m]),
|
|
)
|
|
.push({
|
|
Arrangement::new(&page.list, &page.display_tabs)
|
|
.on_select(|id| pages::Message::Displays(Message::Display(id)))
|
|
.on_pan(|pan| pages::Message::Displays(Message::Pan(pan)))
|
|
.on_placement(|id, x, y| {
|
|
pages::Message::Displays(Message::Position(id, x, y))
|
|
})
|
|
.apply(widget::scrollable::horizontal)
|
|
.id(page.display_arrangement_scrollable.clone())
|
|
.width(Length::Shrink)
|
|
.apply(container)
|
|
.center_x(Length::Fill)
|
|
})
|
|
.apply(container)
|
|
.class(cosmic::theme::Container::List)
|
|
.width(Length::Fill)
|
|
.into()
|
|
})
|
|
}
|
|
|
|
/// View for the display configuration section.
|
|
pub fn display_configuration() -> Section<crate::pages::Message> {
|
|
crate::slab!(descriptions {
|
|
_display = fl!("display");
|
|
refresh_rate = fl!("display", "refresh-rate");
|
|
vrr = fl!("vrr");
|
|
resolution = fl!("display", "resolution");
|
|
scale = fl!("display", "scale");
|
|
additional_scale_options = fl!("display", "additional-scale-options");
|
|
orientation = fl!("orientation");
|
|
enable_label = fl!("display", "enable");
|
|
options_label = fl!("display", "options");
|
|
mirroring_label = fl!("mirroring");
|
|
});
|
|
|
|
Section::default()
|
|
.descriptions(descriptions)
|
|
.view::<Page>(move |_binder, page, section| {
|
|
let descriptions = §ion.descriptions;
|
|
|
|
let Some(&active_id) = page.display_tabs.active_data::<OutputKey>() else {
|
|
return widget::space().into();
|
|
};
|
|
|
|
let active_output = &page.list.outputs[active_id];
|
|
|
|
let display_options = (page.show_display_options && active_output.enabled).then(|| {
|
|
let mut items = vec![
|
|
widget::settings::item(
|
|
&descriptions[resolution],
|
|
dropdown::popup_dropdown(
|
|
&page.cache.resolutions,
|
|
page.cache.resolution_selected,
|
|
Message::Resolution,
|
|
cosmic::iced::window::Id::RESERVED,
|
|
Message::Surface,
|
|
|a| {
|
|
crate::app::Message::PageMessage(crate::pages::Message::Displays(a))
|
|
},
|
|
),
|
|
),
|
|
widget::settings::item(
|
|
&descriptions[refresh_rate],
|
|
dropdown::popup_dropdown(
|
|
&page.cache.refresh_rates,
|
|
page.cache.refresh_rate_selected,
|
|
Message::RefreshRate,
|
|
cosmic::iced::window::Id::RESERVED,
|
|
Message::Surface,
|
|
|a| {
|
|
crate::app::Message::PageMessage(crate::pages::Message::Displays(a))
|
|
},
|
|
),
|
|
),
|
|
];
|
|
|
|
if let Some(vrr_selected) = page.cache.vrr_selected {
|
|
items.push(widget::settings::item(
|
|
&descriptions[vrr],
|
|
dropdown::popup_dropdown(
|
|
&page.cache.vrr_modes,
|
|
Some(vrr_selected),
|
|
Message::VariableRefreshRate,
|
|
cosmic::iced::window::Id::RESERVED,
|
|
Message::Surface,
|
|
|a| {
|
|
crate::app::Message::PageMessage(crate::pages::Message::Displays(a))
|
|
},
|
|
),
|
|
));
|
|
}
|
|
|
|
items.extend(vec![
|
|
widget::settings::item(
|
|
&descriptions[scale],
|
|
dropdown::popup_dropdown(
|
|
DPI_SCALE_LABELS.as_slice(),
|
|
page.cache.scale_selected,
|
|
Message::Scale,
|
|
cosmic::iced::window::Id::RESERVED,
|
|
Message::Surface,
|
|
|a| {
|
|
crate::app::Message::PageMessage(crate::pages::Message::Displays(a))
|
|
},
|
|
),
|
|
),
|
|
widget::settings::item(
|
|
&descriptions[additional_scale_options],
|
|
widget::spin_button(
|
|
format!("{}%", page.adjusted_scale),
|
|
"additional display scale",
|
|
page.adjusted_scale,
|
|
5,
|
|
0,
|
|
20,
|
|
Message::AdjustScale,
|
|
),
|
|
),
|
|
widget::settings::item(
|
|
&descriptions[orientation],
|
|
dropdown::popup_dropdown(
|
|
&page.cache.orientations,
|
|
page.cache.orientation_selected,
|
|
|id| {
|
|
Message::Orientation(match id {
|
|
0 => Transform::Normal,
|
|
1 => Transform::Rotate90,
|
|
2 => Transform::Rotate180,
|
|
_ => Transform::Rotate270,
|
|
})
|
|
},
|
|
cosmic::iced::window::Id::RESERVED,
|
|
Message::Surface,
|
|
|a| {
|
|
crate::app::Message::PageMessage(crate::pages::Message::Displays(a))
|
|
},
|
|
),
|
|
),
|
|
]);
|
|
|
|
items
|
|
});
|
|
|
|
let mut content = column::with_capacity(2).spacing(cosmic::theme::spacing().space_xs);
|
|
|
|
if page.list.outputs.len() > 1 {
|
|
let display_switcher = tab_bar::horizontal(&page.display_tabs)
|
|
.button_alignment(Alignment::Center)
|
|
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
|
|
.on_activate(Message::Display);
|
|
|
|
let mut display_enable = if page
|
|
// Don't allow disabling display if it's the only active
|
|
.list
|
|
.outputs
|
|
.values()
|
|
.filter(|display| display.enabled)
|
|
.count()
|
|
> 1
|
|
|| !active_output.enabled
|
|
{
|
|
list_column()
|
|
.add(
|
|
widget::settings::item::builder(&descriptions[enable_label])
|
|
.toggler(active_output.enabled, Message::DisplayToggle),
|
|
)
|
|
.add(widget::settings::item(
|
|
&descriptions[mirroring_label],
|
|
widget::dropdown::multi::dropdown(
|
|
&page.mirror_menu,
|
|
Message::Mirroring,
|
|
),
|
|
))
|
|
} else {
|
|
list_column()
|
|
};
|
|
|
|
if let Some(items) = display_options {
|
|
for item in items {
|
|
display_enable = display_enable.add(item);
|
|
}
|
|
}
|
|
|
|
content = content.push(display_switcher).push(display_enable);
|
|
} else {
|
|
content = content
|
|
.push(text::heading(&descriptions[options_label]))
|
|
.push_maybe(display_options.map(|items| {
|
|
let mut column = list_column();
|
|
for item in items {
|
|
column = column.add(item);
|
|
}
|
|
column
|
|
}));
|
|
}
|
|
|
|
content.apply(Element::from).map(pages::Message::Displays)
|
|
})
|
|
}
|
|
|
|
fn cache_rates(cached_rates: &mut Vec<String>, rates: &[u32]) {
|
|
cached_rates.clear();
|
|
|
|
#[inline]
|
|
fn round(rate: &u32) -> u32 {
|
|
(*rate as f32 / 1000.0).round() as u32
|
|
}
|
|
|
|
#[inline]
|
|
fn format_dec(rate: &u32) -> String {
|
|
format!("{:>3}.{:02} Hz", rate / 1000, rate % 1000)
|
|
}
|
|
|
|
for (i, rate) in rates.iter().enumerate() {
|
|
let prev = match i.checked_sub(1) {
|
|
Some(i) => rates.get(i),
|
|
None => None,
|
|
};
|
|
|
|
match (prev, rates.get(i + 1)) {
|
|
(None, None) => cached_rates.push(format!("{} Hz", round(rate))),
|
|
(None, Some(next)) => {
|
|
if round(rate) == round(next) {
|
|
cached_rates.push(format_dec(rate))
|
|
} else {
|
|
cached_rates.push(format!("{} Hz", round(rate)))
|
|
}
|
|
}
|
|
(Some(prev), None) => {
|
|
if round(rate) == round(prev) {
|
|
cached_rates.push(format_dec(rate))
|
|
} else {
|
|
cached_rates.push(format!("{} Hz", round(rate)))
|
|
}
|
|
}
|
|
(Some(prev), Some(next)) => {
|
|
if round(rate) == round(prev) || round(rate) == round(next) {
|
|
cached_rates.push(format_dec(rate))
|
|
} else {
|
|
cached_rates.push(format!("{} Hz", round(rate)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn on_enter() -> crate::pages::Message {
|
|
let randr_fut = cosmic_randr_shell::list();
|
|
|
|
crate::pages::Message::Displays(Message::Update {
|
|
randr: Arc::new(randr_fut.await),
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn test_cache_rates() {
|
|
let rates: &[u32] = &[10000, 11006, 100004, 100005];
|
|
|
|
let mut cached = vec![];
|
|
|
|
cache_rates(&mut cached, rates);
|
|
|
|
assert_eq!(cached, vec!["10 Hz", "11 Hz", "100.04 Hz", "100.05 Hz"])
|
|
}
|