feat: add display settings page
This commit is contained in:
parent
5907e46555
commit
c00b41a463
21 changed files with 2307 additions and 398 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue