feat: add display settings page
This commit is contained in:
parent
5907e46555
commit
c00b41a463
21 changed files with 2307 additions and 398 deletions
741
Cargo.lock
generated
741
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
22
Cargo.toml
22
Cargo.toml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
)+
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
600
app/src/pages/desktop/display/arrangement.rs
Normal file
600
app/src/pages/desktop/display/arrangement.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
246
app/src/pages/desktop/display/graphics.rs
Normal file
246
app/src/pages/desktop/display/graphics.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
790
app/src/pages/desktop/display/mod.rs
Normal file
790
app/src/pages/desktop/display/mod.rs
Normal 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),
|
||||
})
|
||||
}
|
||||
97
app/src/pages/desktop/display/text.rs
Normal file
97
app/src/pages/desktop/display/text.rs
Normal 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");
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
2
debian/control
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
justfile
6
justfile
|
|
@ -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}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue