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

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 {