feat: add display settings page

This commit is contained in:
Michael Aaron Murphy 2023-12-22 16:42:56 +01:00 committed by Michael Murphy
parent 5907e46555
commit c00b41a463
21 changed files with 2307 additions and 398 deletions

741
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,12 +2,9 @@
members = ["app", "page", "pages/*"]
default-members = ["app"]
[workspace.dependencies.iced_core]
git = "https://github.com/pop-os/libcosmic"
[workspace.dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic"
features = ["single-instance", "tokio", "wayland", "xdg-portal"]
features = ["single-instance", "tokio", "wayland", "wgpu", "xdg-portal"]
[workspace.dependencies.cosmic-config]
git = "https://github.com/pop-os/libcosmic"
@ -22,17 +19,12 @@ git = "https://github.com/pop-os/cosmic-comp"
[workspace.dependencies.cosmic-panel-config]
git = "https://github.com/pop-os/cosmic-panel"
# [patch."https://github.com/pop-os/libcosmic"]
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "refactor-single-instance" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "refactor-single-instance"}
[patch."https://github.com/Smithay/client-toolkit"]
sctk = { git = "https://github.com/smithay/client-toolkit//", package = "smithay-client-toolkit", rev = "e63ab5f"}
[workspace.dependencies.sctk]
git = "https://github.com/smithay/client-toolkit/"
package = "smithay-client-toolkit"
rev = "e63ab5f"
[profile.release]
opt-level = 3
debug = true
lto = "thin"
# debug = true
# lto = "thin"

View file

@ -7,8 +7,9 @@ rust-version = "1.65.0"
[dependencies]
apply = "0.3.0"
async-channel = "1.8.0"
async-channel = "1.9.0"
color-eyre = "0.6.2"
cosmic-randr-shell = { git = "https://github.com/pop-os/cosmic-randr", rev = "c35172c" }
cosmic-settings-desktop = { path = "../pages/desktop" }
cosmic-settings-page = { path = "../page" }
cosmic-settings-system = { path = "../pages/system" }
@ -16,33 +17,38 @@ cosmic-settings-time = { path = "../pages/time" }
derivative = "2.2.0"
derive_setters = "0.1.6"
dirs = "5.0.1"
generator = "0.7.4"
generator = "0.7.5"
i18n-embed-fl = "0.6.7"
itertools = "0.11.0"
libcosmic = {workspace = true}
once_cell = "1.17.2"
regex = "1.8.3"
rust-embed = "6.6.1"
slotmap = "1.0.6"
tokio = "1.28.2"
once_cell = "1.19.0"
regex = "1.10.2"
rust-embed = "6.8.1"
slotmap = "1.0.7"
tokio = "1.35.1"
downcast-rs = "1.2.0"
cosmic-comp-config = { workspace = true }
# TODO: migrate this dependency to the pages/desktop crate.
cosmic-panel-config = { workspace = true }
tracing = "0.1.37"
tracing-subscriber = "0.3.17"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
log = "0.4"
url = "2.3.1"
url = "2.5.0"
freedesktop-desktop-entry = "0.5.0"
notify = "6.0.0"
notify = "6.1.1"
anyhow = "1.0"
image = "0.24.6"
serde = { version = "1.0.180", features = ["derive"] }
ashpd = { version = "0.6.2", default-features = false }
image = "0.24.7"
serde = { version = "1.0.195", features = ["derive"] }
ashpd = { version = "0.6.8", default-features = false }
ron = "0.8"
static_init = "1.0.3"
clap = {version = "4.4.8", features = ["derive"] }
clap = {version = "4.4.15", features = ["derive"] }
itoa = "1.0.10"
futures = { package = "futures-lite", version = "2.2.0" }
[dependencies.i18n-embed]
version = "0.13.9"
features = ["fluent-system", "desktop-requester"]
[features]
test = []

View file

@ -331,6 +331,12 @@ impl cosmic::Application for SettingsApp {
page::update!(self.pages, message, desktop::workspaces::Page);
}
crate::pages::Message::Displays(message) => {
if let Some(page) = self.pages.page_mut::<desktop::display::Page>() {
return page.update(message).map(cosmic::app::Message::App);
}
}
crate::pages::Message::Input(message) => {
if let Some(page) = self.pages.page_mut::<input::Page>() {
return page.update(message).map(cosmic::app::Message::App);

View file

@ -84,6 +84,8 @@ pub fn main() -> color_eyre::Result<()> {
std::env::set_var("RUST_SPANTRACE", "0");
}
std::env::set_var("WGPU_POWER_PREF", "low");
init_logger();
init_localizer();
@ -133,7 +135,6 @@ fn init_logger() {
tracing_subscriber::registry().with(log_filter).init();
}
#[macro_export]
macro_rules! cache_dynamic_lazy {
( $( $visible:vis static $variable:ident: $type:ty = $expression:expr; )+ ) => {
@ -142,4 +143,4 @@ macro_rules! cache_dynamic_lazy {
$visible static $variable: $type = $expression;
)+
};
}
}

View file

@ -91,7 +91,7 @@ impl From<(Option<Config>, ThemeMode, Option<Config>, ThemeBuilder)> for Page {
ThemeBuilder,
),
) -> Self {
let theme: Theme<palette::Srgba> = if theme_mode.is_dark {
let theme = if theme_mode.is_dark {
Theme::dark_default()
} else {
Theme::light_default()
@ -615,9 +615,9 @@ impl Page {
};
let config = if self.theme_mode.is_dark {
Theme::<Srgba>::dark_config()
Theme::dark_config()
} else {
Theme::<Srgba>::light_config()
Theme::light_config()
};
let new_theme = self.theme_builder.clone().build();
if let Ok(config) = config {
@ -757,9 +757,9 @@ impl Page {
};
let config = if self.theme_mode.is_dark {
Theme::<Srgba>::dark_config()
Theme::dark_config()
} else {
Theme::<Srgba>::light_config()
Theme::light_config()
};
let new_theme = self.theme_builder.clone().build();
if let Ok(config) = config {
@ -776,7 +776,7 @@ impl Page {
Message::UseDefaultWindowHint(v) => {
self.no_custom_window_hint = v;
theme_builder_needs_update = true;
let theme: Theme<palette::Srgba> = if self.theme_mode.is_dark {
let theme = if self.theme_mode.is_dark {
Theme::dark_default()
} else {
Theme::light_default()
@ -840,9 +840,9 @@ impl Page {
self.theme_builder = theme_builder;
let config = if self.theme_mode.is_dark {
Theme::<Srgba>::dark_config()
Theme::dark_config()
} else {
Theme::<Srgba>::light_config()
Theme::light_config()
};
if let Ok(config) = config {
let new_theme = self.theme_builder.clone().build();

View file

@ -0,0 +1,600 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::iced_core::renderer::Quad;
use cosmic::iced_core::widget::{tree, Tree};
use cosmic::iced_core::{
self as core, Clipboard, Element, Layout, Length, Rectangle, Renderer as IcedRenderer, Shell,
Size, Widget,
};
use cosmic::iced_core::{alignment, event, text};
use cosmic::iced_core::{layout, mouse, renderer, touch, Point};
use cosmic::widget::segmented_button::{self, SingleSelectModel};
use cosmic::Renderer;
use cosmic_randr_shell::{self as randr, OutputKey};
use randr::Transform;
const UNIT_PIXELS: f32 = 12.0;
pub type OnPlacementFunc<Message> = Box<dyn Fn(OutputKey, i32, i32) -> Message>;
pub type OnSelectFunc<Message> = Box<dyn Fn(segmented_button::Entity) -> Message>;
#[must_use]
#[derive(derive_setters::Setters)]
pub struct Arrangement<'a, Message> {
#[setters(skip)]
list: &'a randr::List,
#[setters(skip)]
tab_model: &'a SingleSelectModel,
#[setters(skip)]
on_placement: Option<OnPlacementFunc<Message>>,
#[setters(skip)]
on_select: Option<OnSelectFunc<Message>>,
width: Length,
height: Length,
}
impl<'a, Message> Arrangement<'a, Message> {
pub fn new(list: &'a randr::List, tab_model: &'a SingleSelectModel) -> Self {
Self {
list,
tab_model,
on_placement: None,
on_select: None,
width: Length::Shrink,
height: Length::Shrink,
}
}
pub fn on_placement(
mut self,
on_placement: impl Fn(OutputKey, i32, i32) -> Message + 'static,
) -> Self {
self.on_placement = Some(Box::new(on_placement));
self
}
pub fn on_select(
mut self,
on_select: impl Fn(segmented_button::Entity) -> Message + 'static,
) -> Self {
self.on_select = Some(Box::new(on_select));
self
}
}
impl<'a, Message: Clone> Widget<Message, Renderer> for Arrangement<'a, Message> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
// Determine the max display dimensions, and the total display area utilized.
let mut max_dimensions = (0, 0);
let mut display_area = (0, 0);
for (_key, output) in self.list.outputs.iter() {
if !output.enabled {
continue;
}
let Some(mode_key) = output.current else {
continue;
};
let Some(mode) = self.list.modes.get(mode_key) else {
continue;
};
let (width, height) = if output.transform.map_or(true, is_landscape) {
(mode.size.0, mode.size.1)
} else {
(mode.size.1, mode.size.0)
};
max_dimensions.0 = max_dimensions.0.max(width);
max_dimensions.1 = max_dimensions.1.max(height);
display_area.0 = display_area.0.max(width as i32 + output.position.0);
display_area.1 = display_area.1.max(height as i32 + output.position.1);
}
let width = (max_dimensions.0 as i32 * 3 + display_area.0) as f32 / UNIT_PIXELS;
let height = (max_dimensions.1 as i32 * 3 + display_area.1) as f32 / UNIT_PIXELS;
let state = tree.state.downcast_mut::<State>();
state.max_dimensions = (
max_dimensions.0 as f32 * 1.25 / UNIT_PIXELS,
max_dimensions.1 as f32 * 1.25 / UNIT_PIXELS,
);
let limits = limits
.width(Length::Fixed(width))
.height(Length::Fixed(height));
let size = limits.resolve(Size::ZERO);
layout::Node::new(size)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: cosmic::iced_core::Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
let bounds = layout.bounds();
match event {
core::Event::Mouse(mouse::Event::CursorMoved { .. })
| core::Event::Touch(touch::Event::FingerMoved { .. }) => {
if let Some(position) = cursor.position() {
let state = tree.state.downcast_mut::<State>();
if let Some((output_key, region)) = state.dragging.as_mut() {
update_dragged_region(
self.tab_model,
self.list,
&bounds,
*output_key,
region,
state.max_dimensions,
(position.x - state.offset.0, position.y - state.offset.1),
);
return event::Status::Captured;
}
}
}
core::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| core::Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(position) = cursor.position() {
let state = tree.state.downcast_mut::<State>();
if let Some((output_key, output_region)) = display_region_hovers(
self.tab_model,
self.list,
&bounds,
state.max_dimensions,
position,
) {
state.drag_from = position;
state.offset = (position.x - output_region.x, position.y - output_region.y);
state.dragging = Some((output_key, output_region));
return event::Status::Captured;
}
}
}
core::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| core::Event::Touch(touch::Event::FingerLifted { .. }) => {
let state = tree.state.downcast_mut::<State>();
if let Some((output_key, region)) = state.dragging.take() {
if let Some(position) = cursor.position() {
if position.distance(state.drag_from) < 4.0 {
if let Some(ref on_select) = self.on_select {
for id in self.tab_model.iter() {
if let Some(&key) = self.tab_model.data::<OutputKey>(id) {
if key == output_key {
shell.publish(on_select(id));
}
}
}
}
return event::Status::Captured;
}
}
if let Some(ref on_placement) = self.on_placement {
shell.publish(on_placement(
output_key,
((region.x - state.max_dimensions.0 - bounds.x) * UNIT_PIXELS) as i32,
((region.y - state.max_dimensions.1 - bounds.y) * UNIT_PIXELS) as i32,
));
}
return event::Status::Captured;
}
}
_ => (),
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
for (_output_key, region) in
display_regions(self.tab_model, self.list, &bounds, state.max_dimensions)
{
if cursor.is_over(region) {
return mouse::Interaction::Grab;
}
}
mouse::Interaction::Idle
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
_theme: &cosmic::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let theme = cosmic::theme::active();
let cosmic_theme = theme.cosmic();
let border_color = cosmic_theme.palette.neutral_7;
let active_key = self.tab_model.active_data::<OutputKey>();
for (id, (output_key, mut region)) in
display_regions(self.tab_model, self.list, &bounds, state.max_dimensions).enumerate()
{
// If the output is being dragged, show its dragged position instead.
if let Some((dragged_key, dragged_region)) = state.dragging {
if dragged_key == output_key {
region = dragged_region;
}
}
let (background, border_color) = if Some(&output_key) == active_key {
let mut border_color = border_color;
border_color.alpha = 0.4;
(cosmic_theme.accent_color(), border_color)
} else {
(cosmic_theme.palette.neutral_4, border_color)
};
renderer.fill_quad(
Quad {
bounds: region,
border_radius: 4.0.into(),
border_width: 3.0,
border_color: border_color.into(),
},
core::Background::Color(background.into()),
);
let id_bounds = Rectangle {
x: region.x + (region.width / 2.0 - 36.0),
y: region.y + (region.height / 2.0 - 23.0),
width: 72.0,
height: 46.0,
};
renderer.fill_quad(
Quad {
bounds: id_bounds,
border_radius: 30.0.into(),
border_width: 0.0,
border_color: core::Color::TRANSPARENT,
},
core::Background::Color(cosmic_theme.palette.neutral_1.into()),
);
core::text::Renderer::fill_text(
renderer,
core::Text {
content: itoa::Buffer::new().format(id),
size: core::Pixels(24.0),
line_height: core::text::LineHeight::Relative(1.2),
font: cosmic::font::FONT_BOLD,
bounds: id_bounds.size(),
horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Center,
shaping: text::Shaping::Basic,
},
core::Point {
x: id_bounds.center_x(),
y: id_bounds.center_y(),
},
cosmic_theme.palette.neutral_10.into(),
*viewport,
);
}
}
}
impl<'a, Message: 'static + Clone> From<Arrangement<'a, Message>> for cosmic::Element<'a, Message> {
fn from(display_positioner: Arrangement<'a, Message>) -> Self {
Element::new(display_positioner)
}
}
#[derive(Default)]
struct State {
drag_from: Point,
dragging: Option<(OutputKey, Rectangle)>,
offset: (f32, f32),
max_dimensions: (f32, f32),
}
/// Iteratively calculate display regions for each display output in the list.
fn display_regions<'a>(
model: &'a SingleSelectModel,
list: &'a randr::List,
bounds: &'a Rectangle,
max_dimensions: (f32, f32),
) -> impl Iterator<Item = (OutputKey, Rectangle)> + 'a {
model
.iter()
.filter_map(move |id| model.data::<OutputKey>(id))
.filter_map(move |&key| {
let Some(output) = list.outputs.get(key) else {
return None;
};
if !output.enabled {
return None;
}
let Some(mode_key) = output.current else {
return None;
};
let Some(mode) = list.modes.get(mode_key) else {
return None;
};
let (mut width, mut height) = (
(mode.size.0 as f32) / UNIT_PIXELS,
(mode.size.1 as f32) / UNIT_PIXELS,
);
(width, height) = if output.transform.map_or(true, is_landscape) {
(width, height)
} else {
(height, width)
};
Some((
key,
Rectangle {
width,
height,
x: max_dimensions.0 + bounds.x + (output.position.0 as f32) / UNIT_PIXELS,
y: max_dimensions.1 + bounds.y + (output.position.1 as f32) / UNIT_PIXELS,
},
))
})
}
fn display_region_hovers(
model: &SingleSelectModel,
list: &randr::List,
bounds: &Rectangle,
max_dimensions: (f32, f32),
point: Point,
) -> Option<(OutputKey, Rectangle)> {
for (output_key, region) in display_regions(model, list, bounds, max_dimensions) {
if region.contains(point) {
return Some((output_key, region));
}
}
None
}
/// Updates a display's region, preventing coordinates from overlapping with existing displays.
fn update_dragged_region(
model: &SingleSelectModel,
list: &randr::List,
bounds: &Rectangle,
output: OutputKey,
region: &mut Rectangle,
max_dimensions: (f32, f32),
(x, y): (f32, f32),
) {
let mut dragged_region = Rectangle { x, y, ..*region };
// Prevent display from exceeding boundaries.
if !dragged_region.is_within_strict(bounds) {
return;
}
let mut nearest = f32::MAX;
let mut nearest_region = Rectangle::default();
let mut nearest_side = NearestSide::East;
// Find the nearest adjacent display to the dragged display.
for (other_output, other_region) in display_regions(model, list, bounds, max_dimensions) {
if other_output == output {
continue;
}
let center = dragged_region.center();
let eastward = distance(east_point(&other_region), center) * 1.5;
let westward = distance(west_point(&other_region), center) * 1.5;
let northward = distance(north_point(&other_region), center);
let southward = distance(south_point(&other_region), center);
let mut nearer = false;
if nearest > eastward {
(nearest, nearest_side, nearer) = (eastward, NearestSide::East, true);
}
if nearest > westward {
(nearest, nearest_side, nearer) = (westward, NearestSide::West, true);
}
if nearest > northward {
(nearest, nearest_side, nearer) = (northward, NearestSide::North, true);
}
if nearest > southward {
(nearest, nearest_side, nearer) = (southward, NearestSide::South, true);
}
if nearer {
nearest_region = other_region;
}
}
// Attach dragged display to nearest adjacent display.
match nearest_side {
NearestSide::East => {
dragged_region.x = nearest_region.x - dragged_region.width;
dragged_region.y = dragged_region
.y
.max(nearest_region.y - dragged_region.height + 8.0)
.min(nearest_region.y + nearest_region.height - 8.0);
}
NearestSide::North => {
dragged_region.y = nearest_region.y - dragged_region.height;
dragged_region.x = dragged_region
.x
.max(nearest_region.x - dragged_region.width + 8.0)
.min(nearest_region.x + nearest_region.width - 8.0);
}
NearestSide::West => {
dragged_region.x = nearest_region.x + nearest_region.width;
dragged_region.y = dragged_region
.y
.max(nearest_region.y - dragged_region.height + 8.0)
.min(nearest_region.y + nearest_region.height - 8.0);
}
NearestSide::South => {
dragged_region.y = nearest_region.y + nearest_region.height;
dragged_region.x = dragged_region
.x
.max(nearest_region.x - dragged_region.width + 8.0)
.min(nearest_region.x + nearest_region.width - 8.0);
}
}
// Snap-align on x-axis when alignment is near.
if (dragged_region.x - nearest_region.x).abs() <= 8.0 {
dragged_region.x = nearest_region.x;
}
// Snap-align on x-axis when alignment is near bottom edge.
if ((dragged_region.x + dragged_region.width) - (nearest_region.x + nearest_region.width)).abs()
<= 8.0
{
dragged_region.x = nearest_region.x + nearest_region.width - dragged_region.width;
}
// Snap-align on y-axis when alignment is near.
if (dragged_region.y - nearest_region.y).abs() <= 8.0 {
dragged_region.y = nearest_region.y;
}
// Snap-align on y-axis when alignment is near bottom edge.
if ((dragged_region.y + dragged_region.height) - (nearest_region.y + nearest_region.height))
.abs()
<= 8.0
{
dragged_region.y = nearest_region.y + nearest_region.height - dragged_region.height;
}
// Prevent display from exceeding boundaries.
if !dragged_region.is_within_strict(bounds) {
return;
}
// Prevent display from overlapping with other displays.
for (other_output, other_region) in display_regions(model, list, bounds, max_dimensions) {
if other_output == output {
continue;
}
if other_region.intersects(&dragged_region) {
return;
}
}
*region = dragged_region;
}
fn is_landscape(transform: Transform) -> bool {
matches!(
transform,
Transform::Normal | Transform::Rotate180 | Transform::Flipped | Transform::Flipped180
)
}
#[derive(Debug)]
enum NearestSide {
East,
North,
South,
West,
}
fn distance(a: Point, b: Point) -> f32 {
((b.x - a.x).powf(2.0) + (b.y - a.y).powf(2.0)).sqrt()
}
fn east_point(r: &Rectangle) -> Point {
Point {
x: r.x,
y: r.center_y(),
}
}
fn north_point(r: &Rectangle) -> Point {
Point {
x: r.center_x(),
y: r.y,
}
}
fn west_point(r: &Rectangle) -> Point {
Point {
x: r.x + r.width,
y: r.center_y(),
}
}
fn south_point(r: &Rectangle) -> Point {
Point {
x: r.center_x(),
y: r.y + r.height,
}
}

View file

@ -0,0 +1,246 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use super::{Message, NightLight};
use crate::pages;
use apply::Apply;
use cosmic::iced_core::{Alignment, Length, Padding};
use cosmic::prelude::CollectionWidget;
use cosmic::widget::{button, column, icon, list_column, row, toggler};
use cosmic::{Command, Element};
use std::sync::Arc;
pub const INTEGRATED: &str = "integrated";
pub const NVIDIA: &str = "nvidia";
pub const HYBRID: &str = "hybrid";
pub const COMPUTE: &str = "compute";
pub async fn fetch() -> Option<Arc<std::io::Result<Mode>>> {
let switchable = tokio::process::Command::new("system76-power")
.args(["graphics", "switchable"])
.output()
.await
.map(|output| {
std::str::from_utf8(&output.stdout).map_or(false, |text| text.trim() == "switchable")
});
match switchable {
Ok(false) => None,
Ok(true) => Some(Arc::new(
tokio::process::Command::new("system76-power")
.arg("graphics")
.output()
.await
.and_then(|output| {
if let Ok(mut mode) = std::str::from_utf8(&output.stdout) {
mode = mode.trim();
if mode == COMPUTE {
Ok(Mode::Compute)
} else if mode == HYBRID {
Ok(Mode::Hybrid)
} else if mode == INTEGRATED {
Ok(Mode::Integrated)
} else if mode == NVIDIA {
Ok(Mode::Nvidia)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"unknown graphics mode",
))
}
} else {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"system76-power output was not UTF-8",
))
}
}),
)),
Err(why) => Some(Arc::new(Err(why))),
}
}
pub fn view(
mode: &'static str,
description: &'static str,
button: Option<(&'static str, Message)>,
) -> Element<'static, Message> {
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let has_checkmark = button.is_none();
let content = column::with_capacity(3)
.padding(Padding::from([theme.space_xxs(), theme.space_l()]))
.push(cosmic::widget::text::body(mode))
.push(cosmic::widget::text::caption(description))
.push(cosmic::widget::Space::new(Length::Fill, 12))
.push_maybe(button.map(|(text, message)| {
button::text(text)
.style(cosmic::theme::Button::Link)
.trailing_icon(icon::from_name("go-next-symbolic").size(16))
.padding(0)
.on_press(message)
}));
if has_checkmark {
row::with_capacity(2)
.align_items(Alignment::Center)
.push(content)
.push(icon::from_name("object-select-symbolic").size(24))
.apply(Element::from)
.apply(cosmic::widget::list::container)
.into()
} else {
cosmic::widget::list::container(content).into()
}
}
/// Switchable graphics mode
#[derive(Clone, Copy, Debug)]
pub enum Mode {
Compute,
Hybrid,
Integrated,
Nvidia,
}
impl Mode {
#[must_use]
pub fn argument_str(self) -> &'static str {
match self {
Self::Compute => COMPUTE,
Self::Hybrid => HYBRID,
Self::Integrated => INTEGRATED,
Self::Nvidia => NVIDIA,
}
}
#[must_use]
pub fn localized_str(self) -> &'static str {
match self {
Self::Compute => &super::text::GRAPHICS_MODE_COMPUTE,
Self::Hybrid => &super::text::GRAPHICS_MODE_HYBRID,
Self::Integrated => &super::text::GRAPHICS_MODE_INTEGRATED,
Self::Nvidia => &super::text::GRAPHICS_MODE_NVIDIA,
}
}
}
impl super::Page {
pub fn graphics_mode_view(&self) -> Element<pages::Message> {
let mut container = list_column();
if let Some(graphics_mode) = self.config.graphics_mode {
// Displays the active graphics mode, and a button for configuring it.
container = container.add(cosmic::widget::settings::item(
&*super::text::GRAPHICS_MODE,
row()
.align_items(Alignment::Center)
.push(cosmic::widget::text::body(graphics_mode.localized_str()))
.push(
button::icon(icon::from_name("go-next-symbolic"))
.extra_small()
.on_press(Message::GraphicsModeContext),
),
));
}
// Displays the night light status, and a button for configuring it.
container = container.add(
cosmic::widget::settings::item::builder(&*super::text::NIGHT_LIGHT)
.description(&*super::text::NIGHT_LIGHT_DESCRIPTION)
.control(
row()
.align_items(Alignment::Center)
.push(toggler(None, self.config.night_light_enabled, |enable| {
Message::NightLight(NightLight::Toggle(enable))
}))
.push(
button::icon(icon::from_name("go-next-symbolic"))
.extra_small()
.on_press(Message::NightLightContext),
),
),
);
container
.apply(Element::from)
.map(crate::pages::Message::Displays)
}
pub fn graphics_mode_context_view(&self) -> Element<pages::Message> {
let theme = cosmic::theme::active();
let theme = theme.cosmic();
column::with_capacity(4)
.spacing(theme.space_xs())
.push(view(
&super::text::GRAPHICS_MODE_INTEGRATED,
&super::text::GRAPHICS_MODE_INTEGRATED_DESC,
if let Some(Mode::Integrated) = self.config.graphics_mode {
None
} else {
Some((
&super::text::GRAPHICS_MODE_INTEGRATED_ENABLE,
Message::GraphicsMode(Mode::Integrated),
))
},
))
.push(view(
&super::text::GRAPHICS_MODE_NVIDIA,
&super::text::GRAPHICS_MODE_NVIDIA_DESC,
if let Some(Mode::Nvidia) = self.config.graphics_mode {
None
} else {
Some((
&super::text::GRAPHICS_MODE_NVIDIA_ENABLE,
Message::GraphicsMode(Mode::Nvidia),
))
},
))
.push(view(
&super::text::GRAPHICS_MODE_HYBRID,
&super::text::GRAPHICS_MODE_HYBRID_DESC,
if let Some(Mode::Hybrid) = self.config.graphics_mode {
None
} else {
Some((
&super::text::GRAPHICS_MODE_HYBRID_ENABLE,
Message::GraphicsMode(Mode::Hybrid),
))
},
))
.push(view(
&super::text::GRAPHICS_MODE_COMPUTE,
&super::text::GRAPHICS_MODE_COMPUTE_DESC,
if let Some(Mode::Compute) = self.config.graphics_mode {
None
} else {
Some((
&super::text::GRAPHICS_MODE_COMPUTE_ENABLE,
Message::GraphicsMode(Mode::Compute),
))
},
))
.apply(Element::from)
.map(pages::Message::Displays)
}
/// Change the graphics mode.
pub fn set_graphics_mode(&mut self, mode: Mode) -> Command<crate::app::Message> {
self.config.graphics_mode = Some(mode);
// Runs `system76-power graphics {{mode}}`
cosmic::command::future(async move {
let result = tokio::process::Command::new("system76-power")
.args(["graphics", mode.argument_str()])
.status()
.await;
let page_message =
crate::pages::Message::Displays(Message::GraphicsModeResult(Arc::new(result)));
crate::app::Message::PageMessage(page_message)
})
}
}

View file

@ -0,0 +1,790 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod arrangement;
pub mod graphics;
pub mod text;
use crate::{app, pages};
use apply::Apply;
use arrangement::Arrangement;
use cosmic::iced::Length;
use cosmic::iced_widget::scrollable::{Direction, Properties};
use cosmic::widget::{
column, container, dropdown, list_column, segmented_button, toggler, view_switcher,
};
use cosmic::{command, Command, Element};
use cosmic_randr_shell::{List, Output, OutputKey, Transform};
use cosmic_settings_page::{self as page, section, Section};
use slotmap::{Key, SlotMap};
use std::collections::BTreeMap;
use std::{process::ExitStatus, sync::Arc};
/// Display color depth options
#[derive(Clone, Copy, Debug)]
pub struct ColorDepth(usize);
/// Identifies the content to display in the context drawer
pub enum ContextDrawer {
GraphicsMode,
NightLight,
}
/// Display mirroring options
#[derive(Clone, Copy, Debug)]
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),
/// Toggles display on or off.
DisplayToggle(bool),
/// Changes the hybrid graphics mode.
GraphicsMode(graphics::Mode),
/// Shows the graphics mode context drawer.
GraphicsModeContext,
/// Status of an applied graphics mode change
GraphicsModeResult(Arc<std::io::Result<ExitStatus>>),
/// 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),
/// Status of an applied display change.
RandrResult(Arc<std::io::Result<ExitStatus>>),
/// Set the refresh rate of a display.
RefreshRate(usize),
/// Set the resolution of a display.
Resolution(usize),
/// Set the preferred scale for a display.
Scale(usize),
/// Refreshes display outputs.
Update {
/// The current graphics mode
graphics: Option<Arc<std::io::Result<graphics::Mode>>>,
/// Available outputs from cosmic-randr.
randr: Arc<Result<List, cosmic_randr_shell::Error>>,
},
}
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)]
enum Randr {
Position(i32, i32),
RefreshRate(u32),
Resolution(u32, u32),
Scale(u32),
Transform(Transform),
Toggle(bool),
}
/// The page struct for the display settings page.
#[derive(Default)]
pub struct Page {
list: List,
display_tabs: segmented_button::SingleSelectModel,
active_display: OutputKey,
config: Config,
cache: ViewCache,
context: Option<ContextDrawer>,
}
#[derive(Default)]
struct Config {
/// Whether night light is enabled.
night_light_enabled: bool,
graphics_mode: Option<graphics::Mode>,
refresh_rate: Option<u32>,
resolution: Option<(u32, u32)>,
scale: u32,
}
/// Cached view content for widgets.
#[derive(Default)]
struct ViewCache {
modes: BTreeMap<(u32, u32), Vec<u32>>,
orientations: [&'static str; 4],
refresh_rates: Vec<String>,
resolutions: Vec<String>,
orientation_selected: Option<usize>,
refresh_rate_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![
// Graphics switching and light mode
sections.insert(
Section::default()
.descriptions(vec![
text::GRAPHICS_MODE.clone(),
text::GRAPHICS_MODE_COMPUTE_DESC.clone(),
text::GRAPHICS_MODE_HYBRID_DESC.clone(),
text::GRAPHICS_MODE_INTEGRATED_DESC.clone(),
text::GRAPHICS_MODE_NVIDIA_DESC.clone(),
text::NIGHT_LIGHT.clone(),
text::NIGHT_LIGHT_AUTO.clone(),
text::NIGHT_LIGHT_DESCRIPTION.clone(),
])
.view::<Page>(|_binder, page, _section| page.graphics_mode_view()),
),
// Display arrangement
sections.insert(
Section::default()
.title(&*text::DISPLAY_ARRANGEMENT)
.descriptions(vec![
text::DISPLAY_ARRANGEMENT.clone(),
text::DISPLAY_ARRANGEMENT_DESC.clone(),
])
.view::<Page>(|_binder, page, _section| page.display_arrangement_view()),
),
// Display configuration
sections.insert(
Section::default()
.descriptions(vec![
text::DISPLAY.clone(),
text::DISPLAY_REFRESH_RATE.clone(),
text::DISPLAY_SCALE.clone(),
text::ORIENTATION.clone(),
text::ORIENTATION_LANDSCAPE.clone(),
text::ORIENTATION_PORTRAIT.clone(),
])
.view::<Page>(|_binder, page, _section| page.display_view()),
),
])
}
fn info(&self) -> page::Info {
page::Info::new("display", "preferences-desktop-display-symbolic")
.title(fl!("display"))
.description(fl!("display", "desc"))
}
#[cfg(not(feature = "test"))]
fn reload(&mut self, _page: page::Entity) -> Command<crate::pages::Message> {
command::future(reload())
}
#[cfg(feature = "test")]
fn reload(&mut self, _page: page::Entity) -> Command<crate::pages::Message> {
command::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(),
physical: (1, 1),
position: (0, 0),
scale: 1.0,
transform: Some(Transform::Normal),
modes: vec![test_mode],
current: Some(test_mode),
});
randr.outputs.insert(cosmic_randr_shell::Output {
name: "Dummy-2".into(),
enabled: true,
make: None,
model: "Test 1".into(),
physical: (1, 1),
position: (1920, 0),
scale: 1.0,
transform: Some(Transform::Normal),
modes: vec![test_mode],
current: Some(test_mode),
});
crate::pages::Message::Displays(Message::Update {
graphics: graphics::fetch().await,
randr: Arc::new(Ok(randr)),
})
})
}
fn context_drawer(&self) -> Option<Element<pages::Message>> {
Some(match self.context {
Some(ContextDrawer::GraphicsMode) => self.graphics_mode_context_view(),
Some(ContextDrawer::NightLight) => self.night_light_context_view(),
None => return None,
})
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Command<app::Message> {
match message {
Message::RandrResult(result) => {
if let Some(Err(why)) = Arc::into_inner(result) {
tracing::error!(?why, "cosmic-randr error");
}
return cosmic::command::future(async {
crate::Message::PageMessage(reload().await)
});
}
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::GraphicsMode(mode) => return self.set_graphics_mode(mode),
Message::GraphicsModeContext => {
self.context = Some(ContextDrawer::GraphicsMode);
return cosmic::command::message(app::Message::OpenContextDrawer(
text::GRAPHICS_MODE.clone().into(),
));
}
Message::GraphicsModeResult(result) => {
if let Some(Err(why)) = Arc::into_inner(result) {
tracing::error!(?why, "system76-power error");
}
}
Message::Mirroring(mirroring) => match mirroring {
Mirroring::Disable => (),
Mirroring::Mirror(target_display) => (),
Mirroring::Project(target_display) => (),
Mirroring::ProjectToAll => (),
},
Message::NightLight(night_light) => {}
Message::NightLightContext => {
self.context = Some(ContextDrawer::NightLight);
return cosmic::command::message(app::Message::OpenContextDrawer(
text::NIGHT_LIGHT.clone().into(),
));
}
Message::Orientation(orientation) => return self.set_orientation(orientation),
Message::Position(display, x, y) => return self.set_position(display, x, y),
Message::RefreshRate(rate) => return self.set_refresh_rate(rate),
Message::Resolution(option) => return self.set_resolution(option),
Message::Scale(scale) => return self.set_scale(scale),
Message::Update { graphics, randr } => {
match graphics.and_then(Arc::into_inner) {
Some(Ok(mode)) => {
self.config.graphics_mode = Some(mode);
}
Some(Err(why)) => {
tracing::error!(?why, "error fetching graphics switching mode");
}
None => (),
}
match Arc::into_inner(randr) {
Some(Ok(outputs)) => self.update_displays(outputs),
Some(Err(why)) => {
tracing::error!(?why, "error fetching displays");
}
None => (),
}
self.cache.orientations = [
text::ORIENTATION_LANDSCAPE.as_str(),
text::ORIENTATION_PORTRAIT.as_str(),
text::ORIENTATION_LANDSCAPE_FLIPPED.as_str(),
text::ORIENTATION_PORTRAIT_FLIPPED.as_str(),
];
}
}
Command::none()
}
/// View for the display arrangement section.
pub fn display_arrangement_view(&self) -> Element<pages::Message> {
column()
.padding(cosmic::iced::Padding::from([16, 24]))
.spacing(10)
.push(cosmic::widget::text::body(&*text::DISPLAY_ARRANGEMENT_DESC))
.push({
Arrangement::new(&self.list, &self.display_tabs)
.on_select(|id| pages::Message::Displays(Message::Display(id)))
.on_placement(|id, x, y| pages::Message::Displays(Message::Position(id, x, y)))
.apply(cosmic::widget::scrollable)
.width(Length::Shrink)
.direction(Direction::Both {
horizontal: Properties::new(),
vertical: Properties::new(),
})
.apply(container)
.center_x()
.width(Length::Fill)
})
.apply(cosmic::widget::list::container)
.into()
}
/// View for the display configuration section.
pub fn display_view(&self) -> Element<pages::Message> {
let Some(&active_id) = self.display_tabs.active_data::<OutputKey>() else {
return column().into()
};
let active_output = &self.list.outputs[active_id];
let display_meta = list_column().add(cosmic::widget::settings::item(
&*text::DISPLAY_ENABLE,
toggler(None, active_output.enabled, Message::DisplayToggle),
));
let display_options = list_column()
.add(cosmic::widget::settings::item(
&*text::DISPLAY_RESOLUTION,
dropdown(
&self.cache.resolutions,
self.cache.resolution_selected,
Message::Resolution,
),
))
.add(cosmic::widget::settings::item(
&*text::DISPLAY_REFRESH_RATE,
dropdown(
&self.cache.refresh_rates,
self.cache.refresh_rate_selected,
Message::RefreshRate,
),
))
.add(cosmic::widget::settings::item(
&*text::DISPLAY_SCALE,
dropdown(
&["50%", "75%", "100%", "125%", "150%", "175%", "200%"],
self.cache.scale_selected,
Message::Scale,
),
))
.add(cosmic::widget::settings::item(
&*text::ORIENTATION,
dropdown(
&self.cache.orientations,
self.cache.orientation_selected,
|id| {
Message::Orientation(match id {
0 => Transform::Normal,
1 => Transform::Rotate90,
2 => Transform::Flipped,
_ => Transform::Flipped90,
})
},
),
));
column()
.spacing(24)
.push(view_switcher::horizontal(&self.display_tabs).on_activate(Message::Display))
.push(display_meta)
.push(cosmic::widget::text::heading(&*text::DISPLAY_OPTIONS))
.push(display_options)
.apply(Element::from)
.map(pages::Message::Displays)
}
/// 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) {
self.active_display = OutputKey::null();
self.display_tabs.clear();
self.list = list;
let sorted_outputs = self
.list
.outputs
.iter()
.map(|(key, output)| (&*output.name, key))
.collect::<BTreeMap<_, _>>();
for (name, id) in sorted_outputs {
let Some(output) = self.list.outputs.get(id) else {
continue
};
let inches =
((output.physical.0.pow(2) + output.physical.1.pow(2)) as f32).sqrt() * 0.039_370_1;
let inches_string = format!("{inches:.1}\"");
self.display_tabs
.insert()
.text(match name {
"eDP-1" | "LVDS1" => {
fl!("display", "laptop", size = inches_string.as_str())
}
output => fl!(
"display",
"external",
size = inches_string.as_str(),
output = output
),
})
.data::<OutputKey>(id);
}
self.display_tabs.activate_position(0);
// Retrieve data for the first, activated display.
self.set_display(self.display_tabs.active());
}
/// Changes the color depth of the active display.
pub fn set_color_depth(&mut self, depth: ColorDepth) -> Command<app::Message> {
unimplemented!()
}
/// Changes the color profile of the active display.
pub fn set_color_profile(&mut self, profile: usize) -> Command<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.scale = (output.scale * 100.0) as u32;
self.cache.modes.clear();
self.cache.refresh_rates.clear();
self.cache.resolutions.clear();
self.cache.orientation_selected = match output.transform {
Some(Transform::Normal) => Some(0),
Some(Transform::Rotate90) => Some(1),
Some(Transform::Flipped) => Some(2),
Some(Transform::Flipped90) => Some(3),
_ => None,
};
self.cache.resolution_selected = None;
self.cache.refresh_rate_selected = None;
self.cache.scale_selected = Some(if self.config.scale < 75 {
0
} else if self.config.scale < 100 {
1
} else if self.config.scale < 125 {
2
} else if self.config.scale < 150 {
3
} else if self.config.scale < 175 {
4
} else if self.config.scale < 200 {
5
} else {
6
});
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_insert_with(Vec::new);
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);
}
}
}
/// Change display orientation.
pub fn set_orientation(&mut self, transform: Transform) -> Command<app::Message> {
let Some(output) = self.list.outputs.get(self.active_display) else {
return Command::none();
};
self.cache.orientation_selected = match transform {
Transform::Normal => Some(0),
Transform::Rotate90 => Some(1),
Transform::Flipped => Some(2),
_ => Some(3),
};
self.exec_randr(output, Randr::Transform(transform))
}
/// Changes the position of the display.
pub fn set_position(&mut self, display: OutputKey, x: i32, y: i32) -> Command<app::Message> {
let Some(output) = self.list.outputs.get_mut(display) else {
return Command::none();
};
output.position = (x, y);
if cfg!(feature = "test") {
tracing::debug!("set position {x},{y}");
return Command::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) -> Command<app::Message> {
let Some(output) = self.list.outputs.get(self.active_display) else {
return Command::none();
};
if let Some(ref resolution) = self.config.resolution {
if let Some(rates) = self.cache.modes.get(resolution) {
if 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));
}
}
}
Command::none()
}
/// Change the resolution of the active display.
pub fn set_resolution(&mut self, option: usize) -> Command<app::Message> {
let Some(output) = self.list.outputs.get(self.active_display) else {
return Command::none();
};
let Some((&resolution, rates)) = self.cache.modes.iter().rev().nth(option) else {
return Command::none();
};
self.cache.refresh_rates.clear();
cache_rates(&mut self.cache.refresh_rates, rates);
let Some(&rate) = rates.first() else {
return Command::none();
};
self.config.refresh_rate = Some(rate);
self.config.resolution = Some(resolution);
self.cache.refresh_rate_selected = Some(0);
self.cache.resolution_selected = Some(option);
self.exec_randr(output, Randr::Resolution(resolution.0, resolution.1))
}
/// Set the scale of the active display.
pub fn set_scale(&mut self, option: usize) -> Command<app::Message> {
let Some(output) = self.list.outputs.get(self.active_display) else {
return Command::none();
};
let scale = (option * 25 + 50) as u32;
self.cache.scale_selected = Some(option);
self.config.scale = scale;
self.exec_randr(output, Randr::Scale(scale))
}
/// Enables or disables the active display.
pub fn toggle_display(&mut self, enable: bool) -> Command<app::Message> {
let Some(output) = self.list.outputs.get_mut(self.active_display) else {
return Command::none();
};
output.enabled = enable;
let output = &self.list.outputs[self.active_display];
self.exec_randr(output, Randr::Toggle(output.enabled))
}
/// Applies a display configuration via `cosmic-randr`.
fn exec_randr(&self, output: &Output, request: Randr) -> Command<app::Message> {
let Some(current) = output.current.and_then(|id| self.list.modes.get(id)) else {
return Command::none();
};
let name = &*output.name;
let mut command = tokio::process::Command::new("cosmic-randr");
match request {
Randr::Position(x, y) => {
command
.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) => {
command
.arg("mode")
.arg("--refresh")
.arg(
&[
itoa::Buffer::new().format(rate / 1000),
".",
itoa::Buffer::new().format(rate % 1000),
]
.concat(),
)
.arg(name)
.arg(itoa::Buffer::new().format(current.size.0))
.arg(itoa::Buffer::new().format(current.size.1));
}
Randr::Resolution(width, height) => {
command
.arg("mode")
.arg(name)
.arg(itoa::Buffer::new().format(width))
.arg(itoa::Buffer::new().format(height));
}
Randr::Scale(scale) => {
command
.arg("mode")
.arg("--scale")
.arg(
&[
itoa::Buffer::new().format(scale / 100),
".",
itoa::Buffer::new().format(scale % 100),
]
.concat(),
)
.arg(name)
.arg(itoa::Buffer::new().format(current.size.0))
.arg(itoa::Buffer::new().format(current.size.1));
}
Randr::Toggle(enable) => {
command
.arg(if enable { "enable" } else { "disable" })
.arg(name);
}
Randr::Transform(transform) => {
command
.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));
}
}
cosmic::command::future(async move {
tracing::debug!(?command, "executing");
app::Message::from(Message::RandrResult(Arc::new(command.status().await)))
})
}
}
fn cache_rates(cached_rates: &mut Vec<String>, rates: &[u32]) {
*cached_rates = rates
.iter()
.map(|&rate| format!("{:>3}.{:02} Hz", rate / 1000, rate % 1000))
.collect();
}
pub async fn reload() -> crate::pages::Message {
let graphics_fut = graphics::fetch();
let randr_fut = cosmic_randr_shell::list();
let (graphics, randr) = futures::future::zip(graphics_fut, randr_fut).await;
crate::pages::Message::Displays(Message::Update {
graphics,
randr: Arc::new(randr),
})
}

View file

@ -0,0 +1,97 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
crate::cache_dynamic_lazy! {
pub static COLOR: String = fl!("color");
pub static COLOR_DEPTH: String = fl!("color", "depth");
pub static COLOR_PROFILE: String = fl!("color", "profile");
pub static COLOR_PROFILES: String = fl!("color", "sidebar");
pub static COLOR_TEMPERATURE: String = fl!("color", "temperature");
pub static DISPLAY: String = fl!("display");
pub static DISPLAY_ARRANGEMENT: String = fl!("display", "arrangement");
pub static DISPLAY_ARRANGEMENT_DESC: String = fl!("display", "arrangement-desc");
pub static DISPLAY_ENABLE: String = fl!("display", "enable");
pub static DISPLAY_EXTERNAL: String = fl!("display", "external");
pub static DISPLAY_LAPTOP: String = fl!("display", "laptop");
pub static DISPLAY_OPTIONS: String = fl!("display", "options");
pub static DISPLAY_REFRESH_RATE: String = fl!("display", "refresh-rate");
pub static DISPLAY_RESOLUTION: String = fl!("display", "resolution");
pub static DISPLAY_SCALE: String = fl!("display", "scale");
pub static GRAPHICS_MODE: String = fl!("graphics-mode");
pub static GRAPHICS_MODE_COMPUTE: String =
fl!("graphics-mode", "mode", mode = super::graphics::COMPUTE);
pub static GRAPHICS_MODE_COMPUTE_ENABLE: String =
fl!("graphics-mode", "enable", mode = super::graphics::COMPUTE);
pub static GRAPHICS_MODE_COMPUTE_DESC: String =
fl!("graphics-mode", "desc", mode = super::graphics::COMPUTE);
pub static GRAPHICS_MODE_HYBRID: String =
fl!("graphics-mode", "mode", mode = super::graphics::HYBRID);
pub static GRAPHICS_MODE_HYBRID_ENABLE: String =
fl!("graphics-mode", "enable", mode = super::graphics::HYBRID);
pub static GRAPHICS_MODE_HYBRID_DESC: String =
fl!("graphics-mode", "desc", mode = super::graphics::HYBRID);
pub static GRAPHICS_MODE_INTEGRATED: String =
fl!("graphics-mode", "mode", mode = super::graphics::INTEGRATED);
pub static GRAPHICS_MODE_INTEGRATED_ENABLE: String = fl!(
"graphics-mode",
"enable",
mode = super::graphics::INTEGRATED
);
pub static GRAPHICS_MODE_INTEGRATED_DESC: String =
fl!("graphics-mode", "desc", mode = super::graphics::INTEGRATED);
pub static GRAPHICS_MODE_NVIDIA: String =
fl!("graphics-mode", "mode", mode = super::graphics::NVIDIA);
pub static GRAPHICS_MODE_NVIDIA_ENABLE: String =
fl!("graphics-mode", "enable", mode = super::graphics::NVIDIA);
pub static GRAPHICS_MODE_NVIDIA_DESC: String =
fl!("graphics-mode", "desc", mode = super::graphics::NVIDIA);
pub static MIRRORING: String = fl!("mirroring");
pub static NIGHT_LIGHT: String = fl!("night-light");
pub static NIGHT_LIGHT_AUTO: String = fl!("night-light", "auto");
pub static NIGHT_LIGHT_DESCRIPTION: String = fl!("night-light", "desc");
pub static ORIENTATION: String = fl!("orientation");
pub static ORIENTATION_LANDSCAPE: String = fl!("orientation", "landscape");
pub static ORIENTATION_LANDSCAPE_FLIPPED: String = fl!("orientation", "landscape-flipped");
pub static ORIENTATION_PORTRAIT: String = fl!("orientation", "portrait");
pub static ORIENTATION_PORTRAIT_FLIPPED: String = fl!("orientation", "portrait-flipped");
pub static SCHEDULING: String = fl!("scheduling");
pub static SCHEDULING_MANUAL: String = fl!("scheduling", "manual");
}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
pub mod appearance;
pub mod display;
pub mod dock;
pub mod notifications;
pub mod options;
@ -26,7 +27,8 @@ impl page::Page<crate::pages::Message> for Page {
impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
page.sub_page::<options::Page>()
page.sub_page::<display::Page>()
.sub_page::<options::Page>()
.sub_page::<wallpaper::Page>()
.sub_page::<appearance::Page>()
.sub_page::<workspaces::Page>()

View file

@ -13,16 +13,17 @@ pub mod time;
#[derive(Clone, Debug)]
pub enum Message {
About(system::about::Message),
Appearance(desktop::appearance::Message),
DateAndTime(time::date::Message),
Desktop(desktop::Message),
Panel(desktop::panel::Message),
Dock(desktop::dock::Message),
DesktopWallpaper(desktop::wallpaper::Message),
PanelApplet(desktop::panel::applets_inner::Message),
DockApplet(desktop::dock::applets::Message),
Appearance(desktop::appearance::Message),
DesktopWorkspaces(desktop::workspaces::Message),
Input(input::Message),
Displays(desktop::display::Message),
Dock(desktop::dock::Message),
DockApplet(desktop::dock::applets::Message),
External { id: String, message: Vec<u8> },
Input(input::Message),
Page(Entity),
Panel(desktop::panel::Message),
PanelApplet(desktop::panel::applets_inner::Message),
}

View file

@ -5,6 +5,23 @@ use cosmic::widget::{settings, text};
use cosmic_settings_page::{self as page, section, Section};
use slotmap::SlotMap;
// crate::cache_dynamic_lazy! {
// pub static SOUND_ALERTS_VOLUME: String = fl!("sound-alerts", "volume");
// pub static SOUND_ALERTS_SOUND: String = fl!("sound-alerts", "sound");
// pub static SOUND_APPLICATIONS_DESC: String = fl!("sound-applications", "desc");
// pub static SOUND_INPUT_VOLUME: String = fl!("sound-input", "volume");
// pub static SOUND_INPUT_DEVICE: String = fl!("sound-input", "device");
// pub static SOUND_INPUT_LEVEL: String = fl!("sound-input", "level");
// pub static SOUND_OUTPUT_VOLUME: String = fl!("sound-output", "volume");
// pub static SOUND_OUTPUT_DEVICE: String = fl!("sound-output", "device");
// pub static SOUND_OUTPUT_LEVEL: String = fl!("sound-output", "level");
// pub static SOUND_OUTPUT_CONFIG: String = fl!("sound-output", "config");
// pub static SOUND_OUTPUT_BALANCE: String = fl!("sound-output", "balance");
// }
#[derive(Default)]
pub struct Page;

View file

@ -1,7 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{iced_widget::core::BorderRadius, theme};
use cosmic::theme;
#[must_use]
pub fn display_container_frame() -> cosmic::theme::Container {

2
debian/control vendored
View file

@ -21,6 +21,6 @@ Homepage: https://github.com/pop-os/cosmic-settings
Package: cosmic-settings
Architecture: amd64 arm64
Depends: ${misc:Depends}, ${shlibs:Depends}
Depends: ${misc:Depends}, ${shlibs:Depends}, cosmic-randr
Description: Settings application for the COSMIC desktop environment
Settings application for the COSMIC desktop environment

View file

@ -60,6 +60,76 @@ window-management = Window Management
.active-hint = Active window hint size
.gaps = Gaps around tiled windows
## Desktop: Display
-requires-restart = Requires restart
color = Color
.depth = Color depth
.profile = Color profile
.sidebar = Color Profiles
.temperature = Color temperature
display = Display
.desc = Manage displays, graphics switching, and night light
.arrangement = Display Arrangement
.arrangement-desc = Drag displays to rearrange them.
.enable = Enable display
.external = { $size } { $output } External Display
.laptop = { $size } Laptop Display
.options = Display Options
.refresh-rate = Refresh rate
.resolution = Resolution
.scale = Scale
graphics-mode = Graphics mode
.mode = { $mode ->
[compute] Compute
*[hybrid] Hybrid
[integrated] Integrated
[nvidia] NVIDIA
} graphics
.enable = Enable { $mode ->
[compute] compute
*[hybrid] hybrid
[integrated] integrated
[nvidia] NVIDIA
} graphics
.desc = { $mode ->
[compute] Uses dedicated graphics for computational workloads only. Disables external displays. { -requires-restart }.
*[hybrid] Applications use integrated graphics unless explicitly requested to use dedicated graphics. { -requires-restart }.
[integrated] Turns off dedicated graphics for a longer battery life and less fan noise.
[nvidia] Better graphical experience and highest power usage. { -requires-restart }.
}
.restart = Restart and switch to { $mode }?
.restart-desc = Switching to { $mode } will close all open applications
mirroring = Mirroring
.id = Mirroring { $id }
.dont = Don't mirror
.mirror = Mirror { $display }
.project = Project to { $display ->
[all] all displays
*[other] { $display }
}
.project-count = Projecting to { $count} other { $count ->
[1] display
*[other] displays
}
night-light = Night Light
.auto = Automatic (sunset to sunrise)
.desc = Reduce blue light with warmer colors.
orientation = Orientation
.landscape = Landscape
.landscape-flipped = Landscape (Flipped)
.portrait = Portrait
.portrait-flipped = Portrait (Flipped)
scheduling = Scheduling
.manual = Manual schedule
## Desktop: Notifications
notifications = Notifications

View file

@ -20,10 +20,11 @@ export RUSTFLAGS := linker-arg + env_var_or_default('RUSTFLAGS', '')
rootdir := ''
prefix := '/usr'
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
default-schema-target := clean(rootdir / prefix) / 'share' / 'cosmic'
# File paths
bin-src := 'target' / 'release' / name
bin-src := cargo-target-dir / 'release' / name
bin-dest := clean(rootdir / prefix) / 'bin' / name
desktop := appid + '.desktop'
@ -116,3 +117,6 @@ version:
# Show the current git commit
git-rev:
@git rev-parse --short HEAD
info:
echo ${RUSTFLAGS}

View file

@ -5,10 +5,10 @@ edition = "2021"
[dependencies]
derive_setters = "0.1.6"
regex = "1.8.3"
slotmap = "1.0.6"
regex = "1.10.2"
slotmap = "1.0.7"
libcosmic = { workspace = true }
generator = "0.7.4"
generator = "0.7.5"
downcast-rs = "1.2.0"
once_cell = "1.17.2"
once_cell = "1.19.0"
url = "2.5.0"

View file

@ -11,10 +11,10 @@ cosmic-config = { workspace = true }
dirs = "5.0.1"
freedesktop-icons = "0.2.4"
futures-lite = "1.13.0"
image = "0.24.6"
image = "0.24.7"
infer = "0.15.0"
rayon = "1.7.0"
sctk = { git = "https://github.com/smithay/client-toolkit/", package = "smithay-client-toolkit", rev = "dc8c4a0"}
tokio = { version = "1.28.0", features = ["sync"] }
tracing = "0.1.37"
rayon = "1.8.0"
sctk = { workspace = true }
tokio = { version = "1.35.1", features = ["sync"] }
tracing = "0.1.40"
wayland-client = "0.31.1"

View file

@ -8,11 +8,11 @@ license = "GPL-3.0-only"
[dependencies]
byte-unit = { version = "4.0.19", default-features = false }
const_format = "0.2.31"
const_format = "0.2.32"
concat-in-place = "1.1.0"
sysinfo = "0.29.0"
memchr = "2.5.0"
sysinfo = "0.29.11"
memchr = "2.7.1"
[dependencies.bumpalo]
version = "3.13.0"
version = "3.14.0"
features = ["collections"]

View file

@ -4,11 +4,11 @@ version = "0.1.0"
edition = "2021"
[dependencies]
icu_calendar = "1.2.0"
icu_timezone = "1.2.0"
icu_calendar = "1.4.0"
icu_timezone = "1.4.0"
timedate-zbus = "0.1.0"
[dependencies.zbus]
version = "3.13.1"
version = "3.14.1"
default-features = false
features = ["tokio"]