feat: refactor the settings page architecture
This commit is contained in:
parent
efdd934e62
commit
c015ad9948
55 changed files with 2212 additions and 1635 deletions
895
Cargo.lock
generated
895
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
46
Cargo.toml
46
Cargo.toml
|
|
@ -1,45 +1,9 @@
|
|||
[package]
|
||||
name = "cosmic-settings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-only"
|
||||
rust-version = "1.65.0"
|
||||
[workspace]
|
||||
members = ["app", "page", "pages/*"]
|
||||
default-members = ["app"]
|
||||
|
||||
[dependencies]
|
||||
apply = "0.3.0"
|
||||
async-channel = "1.8.0"
|
||||
bytecheck = "0.6.9"
|
||||
color-eyre = "0.6.2"
|
||||
derive_setters = "0.1.5"
|
||||
dirs = "4.0.0"
|
||||
generator = "0.7.2"
|
||||
i18n-embed-fl = "0.6.5"
|
||||
once_cell = "1.17.0"
|
||||
regex = "1.7.1"
|
||||
rkyv = { version = "0.7.39", features = ["validation"]}
|
||||
rust-embed = "6.4.2"
|
||||
slotmap = "1.0.6"
|
||||
tokio = "1.25.0"
|
||||
|
||||
[dependencies.cosmic-settings-system]
|
||||
path = "pages/system"
|
||||
|
||||
[dependencies.i18n-embed]
|
||||
version = "0.13.8"
|
||||
features = ["fluent-system", "desktop-requester"]
|
||||
|
||||
[dependencies.libcosmic]
|
||||
[workspace.dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic"
|
||||
rev = "843919e44f0a00c33c29358359be5b4bfa41ab00"
|
||||
rev = "8232e1d249a467673dbc5b0aa2f2e1665fb18dde"
|
||||
default-features = false
|
||||
features = ["debug", "winit", "dyrend", "tokio"]
|
||||
|
||||
[profile.dev]
|
||||
opt-level = "s"
|
||||
incremental = true
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
overflow-checks = true
|
||||
lto = "thin"
|
||||
incremental = false
|
||||
|
|
|
|||
|
|
@ -41,13 +41,10 @@ Translation files may be found in the [i18n directory](./i18n). New translations
|
|||
|
||||
Licensed under the [GNU Public License 3.0](https://choosealicense.com/licenses/gpl-3.0).
|
||||
|
||||
|
||||
|
||||
### Contribution
|
||||
|
||||
Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the GNU Public License 3.0 (GPL-3.0). Each source file should have a SPDX copyright notice at the top of the file:
|
||||
|
||||
```
|
||||
// Copyright {year-created} System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
```
|
||||
|
|
|
|||
37
app/Cargo.toml
Normal file
37
app/Cargo.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "cosmic-settings"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-only"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[dependencies]
|
||||
apply = "0.3.0"
|
||||
async-channel = "1.8.0"
|
||||
bytecheck = "0.6.10"
|
||||
color-eyre = "0.6.2"
|
||||
cosmic-settings-page = { path = "../page" }
|
||||
cosmic-settings-system = { path = "../pages/system" }
|
||||
cosmic-settings-time = { path = "../pages/time" }
|
||||
derive_setters = "0.1.5"
|
||||
dirs = "4.0.0"
|
||||
generator = "0.7.4"
|
||||
i18n-embed-fl = "0.6.6"
|
||||
libcosmic = {workspace = true}
|
||||
once_cell = "1.17.1"
|
||||
regex = "1.8.1"
|
||||
rkyv = { version = "0.7.41", features = ["validation"]}
|
||||
rust-embed = "6.6.1"
|
||||
slotmap = "1.0.6"
|
||||
tokio = "1.27.0"
|
||||
downcast-rs = "1.2.0"
|
||||
|
||||
[dependencies.i18n-embed]
|
||||
version = "0.13.8"
|
||||
features = ["fluent-system", "desktop-requester"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
overflow-checks = true
|
||||
lto = "thin"
|
||||
incremental = false
|
||||
0
app/README.md
Normal file
0
app/README.md
Normal file
|
|
@ -1,4 +1,4 @@
|
|||
fallback_language = "en"
|
||||
|
||||
[fluent]
|
||||
assets_dir = "i18n"
|
||||
assets_dir = "../i18n"
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
use apply::Apply;
|
||||
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
|
||||
use cosmic::{
|
||||
iced::widget::{self, column, container, horizontal_space, row},
|
||||
iced::{self, Application, Command, Length, Subscription},
|
||||
|
|
@ -19,7 +21,7 @@ use cosmic::{
|
|||
|
||||
use crate::{
|
||||
config::{self, Config},
|
||||
page::{self, desktop, section, sound, system, time, Page},
|
||||
pages::{desktop, sound, system, time},
|
||||
widget::{page_title, parent_page_button, search_header, sub_page_button},
|
||||
};
|
||||
|
||||
|
|
@ -34,7 +36,7 @@ pub struct SettingsApp {
|
|||
pub nav_bar_toggled_condensed: bool,
|
||||
pub nav_bar_toggled: bool,
|
||||
pub nav_bar: segmented_button::SingleSelectModel,
|
||||
pub pages: page::Model,
|
||||
pub pages: page::Binder<crate::pages::Message>,
|
||||
pub scaling_factor: f32,
|
||||
pub search: search::Model,
|
||||
pub search_selections: Vec<(page::Entity, section::Entity)>,
|
||||
|
|
@ -48,10 +50,7 @@ pub struct SettingsApp {
|
|||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
About(system::about::Message),
|
||||
Close,
|
||||
DateAndTime(time::date::Message),
|
||||
Desktop(desktop::Message),
|
||||
Drag,
|
||||
KeyboardNav(keyboard_nav::Message),
|
||||
Maximize,
|
||||
|
|
@ -59,6 +58,7 @@ pub enum Message {
|
|||
NavBar(segmented_button::Entity),
|
||||
None,
|
||||
Page(page::Entity),
|
||||
PageMessage(crate::pages::Message),
|
||||
Search(search::Message),
|
||||
ToggleNavBar,
|
||||
ToggleNavBarCondensed,
|
||||
|
|
@ -71,7 +71,7 @@ impl Application for SettingsApp {
|
|||
type Message = Message;
|
||||
type Theme = Theme;
|
||||
|
||||
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
|
||||
fn new(_: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
let mut config_path = config::PathManager::new();
|
||||
|
||||
let mut app = SettingsApp {
|
||||
|
|
@ -83,7 +83,7 @@ impl Application for SettingsApp {
|
|||
nav_bar: segmented_button::Model::default(),
|
||||
nav_bar_toggled: true,
|
||||
nav_bar_toggled_condensed: false,
|
||||
pages: page::Model::default(),
|
||||
pages: page::Binder::default(),
|
||||
title: crate::fl!("app"),
|
||||
scaling_factor: std::env::var("COSMIC_SCALE")
|
||||
.ok()
|
||||
|
|
@ -101,7 +101,7 @@ impl Application for SettingsApp {
|
|||
// app.insert_page::<networking::Page>();
|
||||
// app.insert_page::<bluetooth::Page>();
|
||||
|
||||
app.insert_page::<desktop::Page>();
|
||||
let desktop_id = app.insert_page::<desktop::Page>().id();
|
||||
|
||||
// app.insert_page::<input::Page>();
|
||||
|
||||
|
|
@ -119,14 +119,14 @@ impl Application for SettingsApp {
|
|||
// app.insert_page::<accessibility::Page>();
|
||||
// app.insert_page::<applications::Page>();
|
||||
|
||||
let mut command = Command::none();
|
||||
let active_id = app
|
||||
.pages
|
||||
.info
|
||||
.iter()
|
||||
.find(|(_id, info)| info.id == *app.config.active_page)
|
||||
.map_or(desktop_id, |(id, _info)| id);
|
||||
|
||||
for (id, info) in app.pages.pages.iter() {
|
||||
if info.id == &*app.config.active_page {
|
||||
command = app.activate_page(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let command = app.activate_page(active_id);
|
||||
|
||||
(app, command)
|
||||
}
|
||||
|
|
@ -195,21 +195,26 @@ impl Application for SettingsApp {
|
|||
self.search_clear();
|
||||
}
|
||||
Message::None | Message::Search(_) => {}
|
||||
Message::About(message) => {
|
||||
if let Some(model) = self.pages.resource_mut::<system::about::Model>() {
|
||||
model.update(message);
|
||||
Message::PageMessage(message) => match message {
|
||||
crate::pages::Message::About(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<system::about::Page>() {
|
||||
page.update(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Desktop(message) => {
|
||||
if let Some(model) = self.pages.resource_mut::<desktop::Model>() {
|
||||
model.update(message);
|
||||
crate::pages::Message::DateAndTime(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<time::date::Page>() {
|
||||
page.update(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::DateAndTime(message) => {
|
||||
if let Some(model) = self.pages.resource_mut::<time::date::Model>() {
|
||||
model.update(message);
|
||||
crate::pages::Message::Desktop(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<desktop::Page>() {
|
||||
page.update(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::pages::Message::External { .. } => {
|
||||
todo!("external plugins not supported yet");
|
||||
}
|
||||
},
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
|
@ -309,7 +314,7 @@ impl SettingsApp {
|
|||
self.active_page = page;
|
||||
|
||||
if current_page != page {
|
||||
self.config.active_page = Box::from(self.pages.pages[page].id);
|
||||
self.config.active_page = Box::from(&*self.pages.info[page].id);
|
||||
self.config_path
|
||||
.config("main", |path| self.config.serialize(path));
|
||||
}
|
||||
|
|
@ -318,12 +323,15 @@ impl SettingsApp {
|
|||
self.search.state = search::State::Inactive;
|
||||
self.activate_navbar(page);
|
||||
|
||||
self.pages.init_page(page).unwrap_or(Command::none())
|
||||
self.pages
|
||||
.page_reload(page)
|
||||
.unwrap_or(Command::none())
|
||||
.map(Message::PageMessage)
|
||||
}
|
||||
|
||||
/// Activates the navbar item associated with a page.
|
||||
fn activate_navbar(&mut self, mut page: page::Entity) {
|
||||
if let Some(parent) = self.pages.pages[page].parent {
|
||||
if let Some(parent) = self.pages.info[page].parent {
|
||||
page = parent;
|
||||
}
|
||||
|
||||
|
|
@ -333,7 +341,9 @@ impl SettingsApp {
|
|||
}
|
||||
|
||||
/// Adds a main page to the settings application.
|
||||
fn insert_page<P: Page>(&mut self) -> page::Insert {
|
||||
fn insert_page<P: page::AutoBind<crate::pages::Message>>(
|
||||
&mut self,
|
||||
) -> page::Insert<crate::pages::Message> {
|
||||
let id = self.pages.register::<P>().id();
|
||||
self.navbar_insert(id);
|
||||
|
||||
|
|
@ -344,25 +354,24 @@ impl SettingsApp {
|
|||
}
|
||||
|
||||
fn navbar_insert(&mut self, id: page::Entity) -> segmented_button::SingleSelectEntityMut {
|
||||
let page = &self.pages.pages[id];
|
||||
let page = &self.pages.info[id];
|
||||
|
||||
self.nav_bar
|
||||
.insert()
|
||||
.text(page.title.clone())
|
||||
.icon(IconSource::from(page.icon_name))
|
||||
.icon(IconSource::from(page.icon_name.clone()))
|
||||
.data(id)
|
||||
.with_id(|nav_id| self.pages.data_set(id, nav_id))
|
||||
}
|
||||
|
||||
/// Displays the view of a page.
|
||||
fn page_view(&self, content: &[section::Entity]) -> cosmic::Element<Message> {
|
||||
let page = &self.pages.pages[self.active_page];
|
||||
|
||||
let page = &self.pages.info[self.active_page];
|
||||
let mut column_widgets = Vec::with_capacity(1);
|
||||
|
||||
if let Some(parent) = page.parent {
|
||||
column_widgets.push(parent_page_button(
|
||||
&self.pages.pages[parent],
|
||||
&self.pages.info[parent],
|
||||
page,
|
||||
Message::Page(parent),
|
||||
));
|
||||
|
|
@ -371,7 +380,11 @@ impl SettingsApp {
|
|||
column_widgets.reserve_exact(1 + content.len());
|
||||
for id in content.iter().copied() {
|
||||
let section = &self.pages.sections[id];
|
||||
column_widgets.push((section.view_fn)(self, section));
|
||||
let model = &self.pages.page[self.active_page];
|
||||
|
||||
column_widgets.push(
|
||||
(section.view_fn)(&self.pages, model.as_ref(), section).map(Message::PageMessage),
|
||||
);
|
||||
}
|
||||
|
||||
settings::view_column(column_widgets).into()
|
||||
|
|
@ -418,13 +431,15 @@ impl SettingsApp {
|
|||
let mut current_page = page::Entity::default();
|
||||
for (page, section) in self.search_selections.iter().copied() {
|
||||
let section = &self.pages.sections[section];
|
||||
let model = &self.pages.page[page];
|
||||
|
||||
if page != current_page {
|
||||
current_page = page;
|
||||
sections.push(search_header(&self.pages, page));
|
||||
}
|
||||
|
||||
let section = (section.view_fn)(self, section)
|
||||
let section = (section.view_fn)(&self.pages, model.as_ref(), section)
|
||||
.map(Message::PageMessage)
|
||||
.apply(iced::widget::container)
|
||||
.padding([0, 0, 0, 48]);
|
||||
|
||||
|
|
@ -436,13 +451,13 @@ impl SettingsApp {
|
|||
|
||||
/// Displays the sub-pages view of a page.
|
||||
fn sub_page_view(&self, sub_pages: &[page::Entity]) -> cosmic::Element<Message> {
|
||||
let page = &self.pages.pages[self.active_page];
|
||||
let page = &self.pages.info[self.active_page];
|
||||
|
||||
let mut column_widgets = Vec::with_capacity(sub_pages.len());
|
||||
column_widgets.push(page_title(page));
|
||||
|
||||
for entity in sub_pages.iter().copied() {
|
||||
let sub_page = &self.pages.pages[entity];
|
||||
let sub_page = &self.pages.info[entity];
|
||||
column_widgets.push(sub_page_button(entity, sub_page));
|
||||
}
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ use once_cell::sync::Lazy;
|
|||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "i18n/"]
|
||||
#[folder = "../i18n/"]
|
||||
struct Localizations;
|
||||
|
||||
pub static LANGUAGE_LOADER: Lazy<FluentLanguageLoader> = Lazy::new(|| {
|
||||
|
|
@ -16,9 +16,9 @@ pub mod localize;
|
|||
|
||||
pub mod widget;
|
||||
|
||||
pub mod page;
|
||||
pub mod pages;
|
||||
|
||||
use cosmic::{iced::Application, settings};
|
||||
use cosmic::iced::Application;
|
||||
use i18n_embed::DesktopLanguageRequester;
|
||||
|
||||
/// # Errors
|
||||
|
|
@ -35,11 +35,11 @@ pub fn main() -> color_eyre::Result<()> {
|
|||
let requested_languages = DesktopLanguageRequester::requested_languages();
|
||||
|
||||
if let Err(error) = localizer.select(&requested_languages) {
|
||||
eprintln!("error while loading fluent localizations: {}", error);
|
||||
eprintln!("error while loading fluent localizations: {error}");
|
||||
}
|
||||
|
||||
settings::set_default_icon_theme("Pop");
|
||||
let mut settings = settings();
|
||||
cosmic::settings::set_default_icon_theme("Pop");
|
||||
let mut settings = cosmic::settings();
|
||||
settings.window.min_size = Some((600, 300));
|
||||
SettingsApp::run(settings)?;
|
||||
|
||||
26
app/src/pages/desktop/appearance.rs
Normal file
26
app/src/pages/desktop/appearance.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct 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![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("appearance", "preferences-pop-desktop-appearance-symbolic")
|
||||
.title(fl!("appearance"))
|
||||
.description(fl!("appearance", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
26
app/src/pages/desktop/dock.rs
Normal file
26
app/src/pages/desktop/dock.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct 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![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("dock", "preferences-pop-desktop-dock-symbolic")
|
||||
.title(fl!("dock"))
|
||||
.description(fl!("dock", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
|
@ -8,18 +8,28 @@ pub mod options;
|
|||
pub mod wallpaper;
|
||||
pub mod workspaces;
|
||||
|
||||
use crate::page;
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub struct Page;
|
||||
#[derive(Debug, Default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Page {
|
||||
pub top_left_hot_corner: bool,
|
||||
pub show_workspaces_button: bool,
|
||||
pub show_applications_button: bool,
|
||||
pub show_minimize_button: bool,
|
||||
pub show_maximize_button: bool,
|
||||
pub slideshow: bool,
|
||||
pub same_background: bool,
|
||||
}
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("desktop", "video-display-symbolic").title(fl!("desktop"))
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("desktop", "video-display-symbolic").title(fl!("desktop"))
|
||||
}
|
||||
}
|
||||
|
||||
fn sub_pages(page: page::Insert) -> page::Insert {
|
||||
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>()
|
||||
.sub_page::<wallpaper::Page>()
|
||||
.sub_page::<appearance::Page>()
|
||||
|
|
@ -40,19 +50,7 @@ pub enum Message {
|
|||
TopLeftHotCorner(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Model {
|
||||
pub top_left_hot_corner: bool,
|
||||
pub show_workspaces_button: bool,
|
||||
pub show_applications_button: bool,
|
||||
pub show_minimize_button: bool,
|
||||
pub show_maximize_button: bool,
|
||||
pub slideshow: bool,
|
||||
pub same_background: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::SameBackground(value) => self.same_background = value,
|
||||
|
|
@ -66,20 +64,20 @@ impl Model {
|
|||
}
|
||||
}
|
||||
|
||||
// impl From<Page> for Message {
|
||||
// fn from(page: Page) -> Message {
|
||||
// Message::Page(page)
|
||||
// impl From<page::Info> for Message {
|
||||
// fn from(page: page::Info) -> Message {
|
||||
// Message::page::Info(page)
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub enum Output {
|
||||
// Page(Page),
|
||||
// page::Info(page::Info),
|
||||
// }
|
||||
|
||||
// impl SubPage for DesktopPage {
|
||||
// impl Subpage::Info for Desktoppage::Info {
|
||||
// //TODO: translate
|
||||
// fn title(&self) -> &'static str {
|
||||
// use DesktopPage::*;
|
||||
// use Desktoppage::Info::*;
|
||||
// match self {
|
||||
// Workspaces => "Workspaces",
|
||||
// Notifications => "Notifications",
|
||||
|
|
@ -88,7 +86,7 @@ impl Model {
|
|||
|
||||
// //TODO: translate
|
||||
// fn description(&self) -> &'static str {
|
||||
// use DesktopPage::*;
|
||||
// use Desktoppage::Info::*;
|
||||
// match self {
|
||||
// Workspaces => "Set workspace number, behavior, and placement.",
|
||||
// Notifications => {
|
||||
|
|
@ -98,18 +96,18 @@ impl Model {
|
|||
// }
|
||||
|
||||
// fn icon_name(&self) -> &'static str {
|
||||
// use DesktopPage::*;
|
||||
// use Desktoppage::Info::*;
|
||||
// match self {
|
||||
// Workspaces => "preferences-pop-desktop-workspaces-symbolic",
|
||||
// Notifications => "preferences-system-notifications-symbolic",
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn parent_page(&self) -> Page {
|
||||
// Page::Desktop(None)
|
||||
// fn parent_page(&self) -> page::Info {
|
||||
// page::Info::Desktop(None)
|
||||
// }
|
||||
|
||||
// fn into_page(self) -> Page {
|
||||
// Page::Desktop(Some(self))
|
||||
// fn into_page(self) -> page::Info {
|
||||
// page::Info::Desktop(Some(self))
|
||||
// }
|
||||
// }
|
||||
26
app/src/pages/desktop/notifications.rs
Normal file
26
app/src/pages/desktop/notifications.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct 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![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("notifications", "preferences-system-notifications-symbolic")
|
||||
.title(fl!("notifications"))
|
||||
.description(fl!("notifications", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
|
@ -10,23 +10,19 @@ use cosmic::{
|
|||
Element,
|
||||
};
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("desktop-options", "video-display-symbolic")
|
||||
.title(fl!("desktop-options"))
|
||||
.description(fl!("desktop-options", "desc"))
|
||||
}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![
|
||||
sections.insert(super_key_action()),
|
||||
sections.insert(hot_corner()),
|
||||
|
|
@ -34,17 +30,25 @@ impl page::Page for Page {
|
|||
sections.insert(window_controls()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("desktop-options", "video-display-symbolic")
|
||||
.title(fl!("desktop-options"))
|
||||
.description(fl!("desktop-options", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hot_corner() -> Section {
|
||||
Section::new()
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
pub fn hot_corner() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("hot-corner"))
|
||||
.descriptions(vec![fl!("hot-corner", "top-left-corner")])
|
||||
.view_fn(|app, section| {
|
||||
let desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let desktop = binder
|
||||
.page::<super::Page>()
|
||||
.expect("desktop page not found");
|
||||
|
||||
let descriptions = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
|
|
@ -54,24 +58,19 @@ pub fn hot_corner() -> Section {
|
|||
}),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(crate::Message::Desktop)
|
||||
.map(crate::pages::Message::Desktop)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn super_key_action() -> Section {
|
||||
Section::new()
|
||||
pub fn super_key_action() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("super-key-action"))
|
||||
.descriptions(vec![
|
||||
fl!("super-key-action", "launcher"),
|
||||
fl!("super-key-action", "workspaces"),
|
||||
fl!("super-key-action", "applications"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let _desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
|
|
@ -91,18 +90,17 @@ pub fn super_key_action() -> Section {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn top_panel() -> Section {
|
||||
Section::new()
|
||||
pub fn top_panel() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("top-panel"))
|
||||
.descriptions(vec![
|
||||
fl!("top-panel", "workspaces"),
|
||||
fl!("top-panel", "applications"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let desktop = binder
|
||||
.page::<super::Page>()
|
||||
.expect("desktop page not found");
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
|
|
@ -123,22 +121,21 @@ pub fn top_panel() -> Section {
|
|||
),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(crate::Message::Desktop)
|
||||
.map(crate::pages::Message::Desktop)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn window_controls() -> Section {
|
||||
Section::new()
|
||||
pub fn window_controls() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("window-controls"))
|
||||
.descriptions(vec![
|
||||
fl!("window-controls", "minimize"),
|
||||
fl!("window-controls", "maximize"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let desktop = binder
|
||||
.page::<super::Page>()
|
||||
.expect("desktop page not found");
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
|
|
@ -159,6 +156,6 @@ pub fn window_controls() -> Section {
|
|||
),
|
||||
))
|
||||
.apply(Element::from)
|
||||
.map(crate::Message::Desktop)
|
||||
.map(crate::pages::Message::Desktop)
|
||||
})
|
||||
}
|
||||
|
|
@ -10,40 +10,43 @@ use cosmic::{
|
|||
widget::{list_column, settings, toggler},
|
||||
Element,
|
||||
};
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
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![sections.insert(settings())])
|
||||
}
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("wallpaper", "preferences-desktop-wallpaper-symbolic")
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("wallpaper", "preferences-desktop-wallpaper-symbolic")
|
||||
.title(fl!("wallpaper"))
|
||||
.description(fl!("wallpaper", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(settings())])
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings() -> Section {
|
||||
Section::new()
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
pub fn settings() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![
|
||||
fl!("wallpaper", "same"),
|
||||
fl!("wallpaper", "fit"),
|
||||
fl!("wallpaper", "slide"),
|
||||
fl!("wallpaper", "change"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
.view::<Page>(|binder, _page, section| {
|
||||
let desktop = binder
|
||||
.page::<super::Page>()
|
||||
.expect("desktop page not found");
|
||||
let descriptions = §ion.descriptions;
|
||||
let desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
let image_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||
|
||||
let mut image_column = Vec::with_capacity(image_paths.len() / 4);
|
||||
|
|
@ -90,6 +93,6 @@ pub fn settings() -> Section {
|
|||
settings::view_column(children)
|
||||
.padding(0)
|
||||
.apply(Element::from)
|
||||
.map(crate::Message::Desktop)
|
||||
.map(crate::pages::Message::Desktop)
|
||||
})
|
||||
}
|
||||
|
|
@ -3,42 +3,41 @@
|
|||
|
||||
use cosmic::iced::{widget::horizontal_space, Length};
|
||||
use cosmic::widget::settings;
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("workspaces", "preferences-pop-desktop-workspaces-symbolic")
|
||||
.title(fl!("workspaces"))
|
||||
.description(fl!("workspaces", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
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![
|
||||
sections.insert(behavior()),
|
||||
sections.insert(multi_behavior()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("workspaces", "preferences-pop-desktop-workspaces-symbolic")
|
||||
.title(fl!("workspaces"))
|
||||
.description(fl!("workspaces", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
fn behavior() -> Section {
|
||||
Section::new()
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn behavior() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("workspaces-behavior"))
|
||||
.descriptions(vec![
|
||||
fl!("workspaces-behavior", "dynamic"),
|
||||
fl!("workspaces-behavior", "fixed"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let _desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
|
||||
settings::view_section(§ion.title)
|
||||
|
|
@ -54,18 +53,14 @@ fn behavior() -> Section {
|
|||
})
|
||||
}
|
||||
|
||||
fn multi_behavior() -> Section {
|
||||
Section::new()
|
||||
fn multi_behavior() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("workspaces-multi-behavior"))
|
||||
.descriptions(vec![
|
||||
fl!("workspaces-multi-behavior", "span"),
|
||||
fl!("workspaces-multi-behavior", "separate"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let _desktop = app
|
||||
.pages
|
||||
.resource::<super::Model>()
|
||||
.expect("desktop model is missing");
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
let descriptions = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
16
app/src/pages/mod.rs
Normal file
16
app/src/pages/mod.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod desktop;
|
||||
pub mod networking;
|
||||
pub mod sound;
|
||||
pub mod system;
|
||||
pub mod time;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
About(system::about::Message),
|
||||
DateAndTime(time::date::Message),
|
||||
Desktop(desktop::Message),
|
||||
External { id: String, message: Vec<u8> },
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page;
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub fn page() -> page::Meta {
|
||||
page::Meta::new("online-accounts", "goa-panel-symbolic")
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("online-accounts", "goa-panel-symbolic")
|
||||
.title(fl!("online-accounts"))
|
||||
.description(fl!("online-accounts", "desc"))
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page;
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub fn page() -> page::Meta {
|
||||
page::Meta::new("wired", "network-workgroup-symbolic")
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("wired", "network-workgroup-symbolic")
|
||||
.title(fl!("wired"))
|
||||
.description(fl!("wired", "desc"))
|
||||
}
|
||||
|
|
@ -1,27 +1,18 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page::{self, Content, Section};
|
||||
use cosmic::{iced, widget::settings};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use super::section;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sound {}
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = Sound;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("sound", "multimedia-volume-control-symbolic")
|
||||
.title(fl!("sound"))
|
||||
.description(fl!("sound", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
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![
|
||||
sections.insert(output()),
|
||||
sections.insert(input()),
|
||||
|
|
@ -29,21 +20,24 @@ impl page::Page for Page {
|
|||
sections.insert(applications()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("sound", "multimedia-volume-control-symbolic")
|
||||
.title(fl!("sound"))
|
||||
.description(fl!("sound", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
fn alerts() -> Section {
|
||||
Section::new()
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
fn alerts() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-alerts"))
|
||||
.descriptions(vec![
|
||||
fl!("sound-alerts", "volume"),
|
||||
fl!("sound-alerts", "sound"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let _sound = app
|
||||
.pages
|
||||
.resource::<Sound>()
|
||||
.expect("sound model is missing");
|
||||
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
§ion.descriptions[0],
|
||||
|
|
@ -57,16 +51,11 @@ fn alerts() -> Section {
|
|||
})
|
||||
}
|
||||
|
||||
fn applications() -> Section {
|
||||
Section::new()
|
||||
fn applications() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-applications"))
|
||||
.descriptions(vec![fl!("sound-applications", "desc")])
|
||||
.view_fn(|app, section| {
|
||||
let _sound = app
|
||||
.pages
|
||||
.resource::<Sound>()
|
||||
.expect("sound model is missing");
|
||||
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
§ion.descriptions[0],
|
||||
|
|
@ -76,20 +65,15 @@ fn applications() -> Section {
|
|||
})
|
||||
}
|
||||
|
||||
fn input() -> Section {
|
||||
Section::new()
|
||||
fn input() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-input"))
|
||||
.descriptions(vec![
|
||||
fl!("sound-input", "volume"),
|
||||
fl!("sound-input", "device"),
|
||||
fl!("sound-input", "level"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let _sound = app
|
||||
.pages
|
||||
.resource::<Sound>()
|
||||
.expect("sound model is missing");
|
||||
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
§ion.descriptions[0],
|
||||
|
|
@ -107,8 +91,8 @@ fn input() -> Section {
|
|||
})
|
||||
}
|
||||
|
||||
fn output() -> Section {
|
||||
Section::new()
|
||||
fn output() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("sound-output"))
|
||||
.descriptions(vec![
|
||||
fl!("sound-output", "volume"),
|
||||
|
|
@ -117,12 +101,7 @@ fn output() -> Section {
|
|||
fl!("sound-output", "config"),
|
||||
fl!("sound-output", "balance"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let _sound = app
|
||||
.pages
|
||||
.resource::<Sound>()
|
||||
.expect("sound model is missing");
|
||||
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(
|
||||
§ion.descriptions[0],
|
||||
158
app/src/pages/system/about.rs
Normal file
158
app/src/pages/system/about.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
|
||||
use cosmic::{
|
||||
iced::{
|
||||
widget::{horizontal_space, row},
|
||||
Length,
|
||||
},
|
||||
widget::{icon, list_column, settings, text},
|
||||
};
|
||||
use cosmic_settings_system::about::Info;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Info(Box<Info>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Page {
|
||||
info: Info,
|
||||
// support_page: page::Entity,
|
||||
}
|
||||
|
||||
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![
|
||||
sections.insert(distributor_logo()),
|
||||
sections.insert(device()),
|
||||
sections.insert(hardware()),
|
||||
sections.insert(os()),
|
||||
sections.insert(related()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("about", "help-about-symbolic")
|
||||
.title(fl!("about"))
|
||||
.description(fl!("about", "desc"))
|
||||
}
|
||||
|
||||
fn load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
|
||||
Some(Box::pin(async move {
|
||||
crate::pages::Message::About(Message::Info(Box::new(Info::load())))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Info(info) => self.info = *info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn device() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.descriptions(vec![fl!("about-device"), fl!("about-device", "desc")])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let desc = §ion.descriptions;
|
||||
let device_name = settings::item::builder(&desc[0])
|
||||
.description(&desc[1])
|
||||
.control(text(&page.info.device_name));
|
||||
|
||||
list_column().add(device_name).into()
|
||||
})
|
||||
}
|
||||
|
||||
fn distributor_logo() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.search_ignore()
|
||||
.view::<Page>(|_binder, _page, _section| {
|
||||
row!(
|
||||
horizontal_space(Length::Fill),
|
||||
icon("distributor-logo", 78),
|
||||
horizontal_space(Length::Fill),
|
||||
)
|
||||
// Add extra padding to reach 40px from the first section.
|
||||
.padding([0, 16, 0, 16])
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn hardware() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("about-hardware"))
|
||||
.descriptions(vec![
|
||||
fl!("about-hardware", "model"),
|
||||
fl!("about-hardware", "memory"),
|
||||
fl!("about-hardware", "processor"),
|
||||
fl!("about-hardware", "graphics"),
|
||||
fl!("about-hardware", "disk-capacity"),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let desc = §ion.descriptions;
|
||||
|
||||
let mut sections = settings::view_section(§ion.title)
|
||||
.add(settings::item(&desc[0], text(&page.info.hardware_model)))
|
||||
.add(settings::item(&desc[1], text(&page.info.memory)))
|
||||
.add(settings::item(&desc[2], text(&page.info.processor)));
|
||||
|
||||
for card in &page.info.graphics {
|
||||
sections = sections.add(settings::item(&desc[3], text(card.as_str())));
|
||||
}
|
||||
|
||||
sections
|
||||
.add(settings::item(&desc[4], text(&page.info.disk_capacity)))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn os() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("about-os"))
|
||||
.descriptions(vec![
|
||||
fl!("about-os", "os"),
|
||||
fl!("about-os", "os-architecture"),
|
||||
fl!("about-os", "desktop-environment"),
|
||||
fl!("about-os", "windowing-system"),
|
||||
])
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
let desc = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&desc[0], text(&page.info.operating_system)))
|
||||
.add(settings::item(&desc[1], text(&page.info.os_architecture)))
|
||||
.add(settings::item(
|
||||
&desc[2],
|
||||
text(&page.info.desktop_environment),
|
||||
))
|
||||
.add(settings::item(&desc[3], text(&page.info.windowing_system)))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn related() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("about-related"))
|
||||
.descriptions(vec![fl!("about-related", "support")])
|
||||
.view::<Page>(|_binder, _page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(§ion.descriptions[0], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
// fn page(app: &crate::SettingsApp) -> &Page {
|
||||
// app.pages
|
||||
// .resource::<Page>()
|
||||
// .expect("missing system->about page")
|
||||
// }
|
||||
26
app/src/pages/system/firmware.rs
Normal file
26
app/src/pages/system/firmware.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct 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![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("firmware", "firmware-manager-symbolic")
|
||||
.title(fl!("firmware"))
|
||||
.description(fl!("firmware", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
25
app/src/pages/system/mod.rs
Normal file
25
app/src/pages/system/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod about;
|
||||
pub mod firmware;
|
||||
pub mod users;
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("system", "system-users-symbolic").title(fl!("system"))
|
||||
}
|
||||
}
|
||||
|
||||
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::<users::Page>()
|
||||
.sub_page::<about::Page>()
|
||||
.sub_page::<firmware::Page>()
|
||||
}
|
||||
}
|
||||
26
app/src/pages/system/users.rs
Normal file
26
app/src/pages/system/users.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct 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![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("users", "system-users-symbolic")
|
||||
.title(fl!("users"))
|
||||
.description(fl!("users", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
|
@ -1,22 +1,49 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
use apply::Apply;
|
||||
use cosmic::{
|
||||
iced::{widget::horizontal_space, Length},
|
||||
widget::settings,
|
||||
};
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
// use icu::calendar::{DateTime, Gregorian};
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Model {
|
||||
pub struct Page {
|
||||
auto: bool,
|
||||
auto_timezone: bool,
|
||||
military_time: bool,
|
||||
// info: Option<cosmic_settings_time::Info>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
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![
|
||||
sections.insert(date()),
|
||||
sections.insert(timezone()),
|
||||
sections.insert(format()),
|
||||
])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("time-date", "preferences-system-time-symbolic")
|
||||
.title(fl!("time-date"))
|
||||
.description(fl!("time-date", "desc"))
|
||||
}
|
||||
|
||||
fn load(&self, _page: page::Entity) -> Option<page::Task<crate::pages::Message>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Automatic(enable) => self.auto = enable,
|
||||
|
|
@ -33,68 +60,40 @@ pub enum Message {
|
|||
MilitaryTime(bool),
|
||||
}
|
||||
|
||||
pub struct Page;
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("time-date", "preferences-system-time-symbolic")
|
||||
.title(fl!("time-date"))
|
||||
.description(fl!("time-date", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![
|
||||
sections.insert(date()),
|
||||
sections.insert(timezone()),
|
||||
sections.insert(format()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn date() -> Section {
|
||||
Section::new()
|
||||
fn date() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("time-date"))
|
||||
.descriptions(vec![fl!("time-date", "auto"), fl!("time-date")])
|
||||
.view_fn(|app, section| {
|
||||
let model = app
|
||||
.pages
|
||||
.resource::<Model>()
|
||||
.expect("time & date model not found");
|
||||
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(
|
||||
settings::item::builder(§ion.descriptions[0])
|
||||
.toggler(model.auto, Message::Automatic),
|
||||
.toggler(page.auto, Message::Automatic),
|
||||
)
|
||||
.add(settings::item(
|
||||
§ion.descriptions[1],
|
||||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::Message::DateAndTime)
|
||||
.map(crate::pages::Message::DateAndTime)
|
||||
})
|
||||
}
|
||||
|
||||
fn format() -> Section {
|
||||
Section::new()
|
||||
fn format() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("time-format"))
|
||||
.descriptions(vec![
|
||||
fl!("time-format", "twenty-four"),
|
||||
fl!("time-format", "first"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let model = app
|
||||
.pages
|
||||
.resource::<Model>()
|
||||
.expect("time & date model not found");
|
||||
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
// 24-hour toggle
|
||||
.add(
|
||||
settings::item::builder(§ion.descriptions[0])
|
||||
.toggler(model.military_time, Message::MilitaryTime),
|
||||
.toggler(page.military_time, Message::MilitaryTime),
|
||||
)
|
||||
// First day of week
|
||||
.add(settings::item(
|
||||
|
|
@ -102,30 +101,25 @@ fn format() -> Section {
|
|||
horizontal_space(Length::Fill),
|
||||
))
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::Message::DateAndTime)
|
||||
.map(crate::pages::Message::DateAndTime)
|
||||
})
|
||||
}
|
||||
|
||||
fn timezone() -> Section {
|
||||
Section::new()
|
||||
fn timezone() -> Section<crate::pages::Message> {
|
||||
Section::default()
|
||||
.title(fl!("time-zone"))
|
||||
.descriptions(vec![
|
||||
fl!("time-zone", "auto"),
|
||||
fl!("time-zone", "auto-info"),
|
||||
fl!("time-zone"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let model = app
|
||||
.pages
|
||||
.resource::<Model>()
|
||||
.expect("time & date model not found");
|
||||
|
||||
.view::<Page>(|_binder, page, section| {
|
||||
settings::view_section(§ion.title)
|
||||
// Automatic timezone toggle
|
||||
.add(
|
||||
settings::item::builder(§ion.descriptions[0])
|
||||
.description(§ion.descriptions[1])
|
||||
.toggler(model.auto_timezone, Message::AutomaticTimezone),
|
||||
.toggler(page.auto_timezone, Message::AutomaticTimezone),
|
||||
)
|
||||
// Time zone select
|
||||
.add(
|
||||
|
|
@ -133,6 +127,6 @@ fn timezone() -> Section {
|
|||
.control(horizontal_space(Length::Fill)),
|
||||
)
|
||||
.apply(cosmic::Element::from)
|
||||
.map(crate::Message::DateAndTime)
|
||||
.map(crate::pages::Message::DateAndTime)
|
||||
})
|
||||
}
|
||||
24
app/src/pages/time/mod.rs
Normal file
24
app/src/pages/time/mod.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub mod date;
|
||||
pub mod region;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("time", "preferences-system-time-symbolic")
|
||||
.title(fl!("time"))
|
||||
.description(fl!("time", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
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::<date::Page>().sub_page::<region::Page>()
|
||||
}
|
||||
}
|
||||
26
app/src/pages/time/region.rs
Normal file
26
app/src/pages/time/region.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page::Section;
|
||||
use cosmic_settings_page::{self as page, section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct 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![sections.insert(Section::default())])
|
||||
}
|
||||
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("time-region", "preferences-desktop-locale-symbolic")
|
||||
.title(fl!("time-region"))
|
||||
.description(fl!("time-region", "desc"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
|
@ -9,17 +9,19 @@ use cosmic::iced::{
|
|||
};
|
||||
use cosmic::widget::{divider, icon, list, settings, text};
|
||||
use cosmic::{theme, Element};
|
||||
|
||||
use crate::page::{self, Meta};
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
#[must_use]
|
||||
pub fn search_header(pages: &page::Model, page: page::Entity) -> cosmic::Element<crate::Message> {
|
||||
let page_meta = &pages.pages[page];
|
||||
pub fn search_header<Message>(
|
||||
pages: &page::Binder<Message>,
|
||||
page: page::Entity,
|
||||
) -> cosmic::Element<crate::Message> {
|
||||
let page_meta = &pages.info[page];
|
||||
|
||||
let mut column_children = Vec::with_capacity(4);
|
||||
|
||||
if let Some(parent) = page_meta.parent {
|
||||
let parent_meta = &pages.pages[parent];
|
||||
let parent_meta = &pages.info[parent];
|
||||
|
||||
column_children.push(
|
||||
text(parent_meta.title.as_str())
|
||||
|
|
@ -52,7 +54,7 @@ pub fn search_page_link<Message: 'static>(title: &str) -> Button<Message, cosmic
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn page_title<Message: 'static>(page: &Meta) -> Element<Message> {
|
||||
pub fn page_title<Message: 'static>(page: &page::Info) -> Element<Message> {
|
||||
row!(
|
||||
text(page.title.as_str()).size(32),
|
||||
horizontal_space(Length::Fill)
|
||||
|
|
@ -62,8 +64,8 @@ pub fn page_title<Message: 'static>(page: &Meta) -> Element<Message> {
|
|||
|
||||
#[must_use]
|
||||
pub fn parent_page_button<'a, Message: Clone + 'static>(
|
||||
parent: &'a Meta,
|
||||
sub_page: &'a Meta,
|
||||
parent: &'a page::Info,
|
||||
sub_page: &'a page::Info,
|
||||
on_press: Message,
|
||||
) -> Element<'a, Message> {
|
||||
column!(
|
||||
|
|
@ -85,10 +87,10 @@ pub fn parent_page_button<'a, Message: Clone + 'static>(
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn sub_page_button(entity: page::Entity, page: &Meta) -> Element<page::Entity> {
|
||||
pub fn sub_page_button(entity: page::Entity, page: &page::Info) -> Element<page::Entity> {
|
||||
settings::item::builder(page.title.as_str())
|
||||
.description(page.description.as_str())
|
||||
.icon(icon(page.icon_name, 20).style(theme::Svg::Symbolic))
|
||||
.icon(icon(&*page.icon_name, 20).style(theme::Svg::Symbolic))
|
||||
.control(row!(
|
||||
horizontal_space(Length::Fill),
|
||||
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic)
|
||||
25
justfile
25
justfile
|
|
@ -15,12 +15,12 @@ rootdir := ''
|
|||
prefix := '/usr'
|
||||
|
||||
# File paths
|
||||
bin-src := 'target/release/' + name
|
||||
bin-dest := rootdir + prefix + '/bin/' + name
|
||||
bin-src := 'target' / 'release' / name
|
||||
bin-dest := clean(rootdir / prefix) / 'bin' / name
|
||||
|
||||
desktop := appid + '.desktop'
|
||||
desktop-src := 'resources/' + desktop
|
||||
desktop-dest := rootdir + prefix + '/share/applications/' + desktop
|
||||
desktop-src := 'resources' / desktop
|
||||
desktop-dest := clean(rootdir / prefix) / 'share' / 'applications' / desktop
|
||||
|
||||
[private]
|
||||
help:
|
||||
|
|
@ -34,21 +34,21 @@ clean:
|
|||
clean-dist: clean
|
||||
rm -rf .cargo vendor vendor.tar target
|
||||
|
||||
# Compile debug build of cosmic-settings
|
||||
# Compile with debug profile
|
||||
build-debug *args:
|
||||
cargo build {{args}}
|
||||
|
||||
# Compile release build of cosmic-settings
|
||||
# Compile with release profile
|
||||
build-release *args: (build-debug '--release' args)
|
||||
|
||||
# Vendored release build of cosmic-settings
|
||||
# Compile with a vendored tarball
|
||||
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
|
||||
|
||||
# Run `cargo clippy`
|
||||
# Check for errors and linter warnings
|
||||
check *args:
|
||||
cargo clippy --all-features {{args}} -- -W clippy::pedantic
|
||||
|
||||
# Run `cargo clippy` with json message format
|
||||
# Runs a check with JSON message format for IDE integration
|
||||
check-json: (check '--message-format=json')
|
||||
|
||||
# Installation command
|
||||
|
|
@ -67,11 +67,11 @@ install: (install-bin bin-src bin-dest) (install-file desktop-src desktop-dest)
|
|||
|
||||
# Run the application for testing purposes
|
||||
run *args:
|
||||
cargo run {{args}}
|
||||
env RUST_BACKTRACE=full cargo run --release {{args}}
|
||||
|
||||
# Run `cargo test`
|
||||
test:
|
||||
cargo test --all-features
|
||||
cargo test
|
||||
|
||||
# Uninstalls everything (requires same arguments as given to install)
|
||||
uninstall:
|
||||
|
|
@ -86,10 +86,9 @@ vendor:
|
|||
tar pcf vendor.tar vendor
|
||||
rm -rf vendor
|
||||
|
||||
# Extracts vendored dependencies if vendor=1
|
||||
# Extracts vendored dependencies
|
||||
[private]
|
||||
vendor-extract:
|
||||
#!/usr/bin/env sh
|
||||
rm -rf vendor
|
||||
tar pxf vendor.tar
|
||||
|
||||
|
|
|
|||
13
page/Cargo.toml
Normal file
13
page/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "cosmic-settings-page"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
derive_setters = "0.1.5"
|
||||
regex = "1.8.1"
|
||||
slotmap = "1.0.6"
|
||||
libcosmic = { workspace = true }
|
||||
generator = "0.7.4"
|
||||
downcast-rs = "1.2.0"
|
||||
once_cell = "1.17.1"
|
||||
7
page/README.md
Normal file
7
page/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# cosmic-settings-page
|
||||
|
||||
This module contains the APIs for creating and managing settings pages.
|
||||
|
||||
- A [Page](./src/lib.rs) implements the `Page` and `AutoBind` traits.
|
||||
- A [Section](./src/section.rs) is a subset of a page, with a view function to generate the UI.
|
||||
- The [Binder](./src/binder.rs) holds all of the pages, their sections, and additional metadata associated with them
|
||||
199
page/src/binder.rs
Normal file
199
page/src/binder.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::section::{self, Section};
|
||||
use crate::{Content, Info, Page};
|
||||
use cosmic::iced_native::command::{Action, Command};
|
||||
use regex::Regex;
|
||||
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
/// All settings pages are registered and managed by the [`Binder`].
|
||||
pub struct Binder<Message> {
|
||||
pub info: SlotMap<crate::Entity, Info>,
|
||||
pub page: SecondaryMap<crate::Entity, Box<dyn Page<Message>>>,
|
||||
pub typed_page_ids: HashMap<TypeId, crate::Entity>,
|
||||
pub resource: HashMap<TypeId, Box<dyn Any>>,
|
||||
pub storage: HashMap<TypeId, SecondaryMap<crate::Entity, Box<dyn Any>>>,
|
||||
pub sub_pages: SparseSecondaryMap<crate::Entity, Vec<crate::Entity>>,
|
||||
pub sections: SlotMap<section::Entity, Section<Message>>,
|
||||
pub content: SparseSecondaryMap<crate::Entity, Content>,
|
||||
}
|
||||
|
||||
impl<Message> Default for Binder<Message> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: SparseSecondaryMap::new(),
|
||||
info: SlotMap::with_key(),
|
||||
page: SecondaryMap::new(),
|
||||
typed_page_ids: HashMap::new(),
|
||||
resource: HashMap::new(),
|
||||
sections: SlotMap::with_key(),
|
||||
storage: HashMap::new(),
|
||||
sub_pages: SparseSecondaryMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: 'static> Binder<Message> {
|
||||
/// Check if a page exists in the model.
|
||||
#[must_use]
|
||||
pub fn contains_item(&self, id: crate::Entity) -> bool {
|
||||
self.info.contains_key(id)
|
||||
}
|
||||
|
||||
/// Returns the content of a page, if it has any.
|
||||
#[must_use]
|
||||
pub fn content(&self, page: crate::Entity) -> Option<&[section::Entity]> {
|
||||
self.content.get(page).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Get an immutable reference to data associated with a page.
|
||||
#[must_use]
|
||||
pub fn data<Data: 'static>(&self, id: crate::Entity) -> Option<&Data> {
|
||||
self.storage
|
||||
.get(&TypeId::of::<Data>())
|
||||
.and_then(|storage| storage.get(id))
|
||||
.and_then(|data| data.downcast_ref())
|
||||
}
|
||||
|
||||
/// Get a mutable reference to data associated with a page.
|
||||
pub fn data_mut<Data: 'static>(&mut self, id: crate::Entity) -> Option<&mut Data> {
|
||||
self.storage
|
||||
.get_mut(&TypeId::of::<Data>())
|
||||
.and_then(|storage| storage.get_mut(id))
|
||||
.and_then(|data| data.downcast_mut())
|
||||
}
|
||||
|
||||
/// Associates data with the item.
|
||||
pub fn data_set<Data: 'static>(&mut self, id: crate::Entity, data: Data) {
|
||||
if self.contains_item(id) {
|
||||
self.storage
|
||||
.entry(TypeId::of::<Data>())
|
||||
.or_insert_with(SecondaryMap::new)
|
||||
.insert(id, Box::new(data));
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a specific data type from the item.
|
||||
pub fn data_remove<Data: 'static>(&mut self, id: crate::Entity) {
|
||||
self.storage
|
||||
.get_mut(&TypeId::of::<Data>())
|
||||
.and_then(|storage| storage.remove(id));
|
||||
}
|
||||
|
||||
/// Registers a new page in the settings panel.
|
||||
pub fn register<P: AutoBind<Message>>(&mut self) -> crate::Insert<Message> {
|
||||
let page = P::default();
|
||||
|
||||
let id = self.register_page(page);
|
||||
|
||||
self.typed_page_ids.insert(TypeId::of::<P>(), id);
|
||||
|
||||
P::sub_pages(crate::Insert { id, model: self })
|
||||
}
|
||||
|
||||
pub fn register_page<P: Page<Message>>(&mut self, page: P) -> crate::Entity {
|
||||
let id = self.info.insert(page.info());
|
||||
|
||||
if let Some(content) = page.content(&mut self.sections) {
|
||||
self.content.insert(id, content);
|
||||
}
|
||||
|
||||
self.page.insert(id, Box::new(page));
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn model(&self, id: crate::Entity) -> Option<&dyn Page<Message>> {
|
||||
self.page.get(id).map(AsRef::as_ref)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn model_mut(&mut self, id: crate::Entity) -> Option<&mut dyn Page<Message>> {
|
||||
self.page.get_mut(id).map(AsMut::as_mut)
|
||||
}
|
||||
|
||||
/// Obtain a reference to a page by its type ID.
|
||||
#[must_use]
|
||||
pub fn page<P: Page<Message>>(&self) -> Option<&P> {
|
||||
let id = self.typed_page_ids.get(&TypeId::of::<P>())?;
|
||||
let page = self.page.get(*id)?;
|
||||
page.downcast_ref::<P>()
|
||||
}
|
||||
|
||||
/// Obtain a reference to a page by its type ID.
|
||||
#[must_use]
|
||||
pub fn page_mut<P: Page<Message>>(&mut self) -> Option<&mut P> {
|
||||
let id = self.typed_page_ids.get(&TypeId::of::<P>())?;
|
||||
let page = self.page.get_mut(*id)?;
|
||||
page.downcast_mut::<P>()
|
||||
}
|
||||
|
||||
/// Calls a page's load function to refresh its data.
|
||||
pub fn page_reload(&mut self, id: crate::Entity) -> Option<Command<Message>> {
|
||||
if let Some(page) = self.page.get(id) {
|
||||
if let Some(future) = page.load(id) {
|
||||
return Some(Command::single(Action::Future(future)));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resource<Resource: 'static>(&self) -> Option<&Resource> {
|
||||
self.resource
|
||||
.get(&TypeId::of::<Resource>())
|
||||
.and_then(|resource| resource.downcast_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resource_mut<Resource: 'static>(&mut self) -> Option<&mut Resource> {
|
||||
self.resource
|
||||
.get_mut(&TypeId::of::<Resource>())
|
||||
.and_then(|resource| resource.downcast_mut())
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
pub fn resource_register<Resource: Default + 'static>(&mut self) {
|
||||
self.resource
|
||||
.entry(TypeId::of::<Resource>())
|
||||
.or_insert_with(|| Box::<Resource>::default());
|
||||
}
|
||||
|
||||
/// Finds content of panels that match the search.
|
||||
pub fn search<'a>(
|
||||
&'a self,
|
||||
rule: &'a Regex,
|
||||
) -> impl Iterator<Item = (crate::Entity, section::Entity)> + 'a {
|
||||
generator::Gn::new_scoped_local(|mut s| {
|
||||
for (page, sections) in self.content.iter() {
|
||||
for id in sections {
|
||||
if self.sections[*id].search_matches(rule) {
|
||||
s.yield_((page, *id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generator::done!();
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the sub-pages of a page, if it has any.
|
||||
pub fn sub_pages(&self, page: crate::Entity) -> Option<&[crate::Entity]> {
|
||||
self.sub_pages.get(page).map(AsRef::as_ref)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AutoBind<Message: 'static>: Page<Message> + Default + 'static {
|
||||
/// Attaches sub-pages to the page.
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
fn sub_pages(page: crate::Insert<Message>) -> crate::Insert<Message> {
|
||||
page
|
||||
}
|
||||
}
|
||||
50
page/src/insert.rs
Normal file
50
page/src/insert.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::{AutoBind, Binder, Content, Entity, Info};
|
||||
|
||||
/// An inserted page which may have additional properties assigned to it.
|
||||
pub struct Insert<'a, Message> {
|
||||
pub model: &'a mut Binder<Message>,
|
||||
pub id: Entity,
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> Insert<'a, Message> {
|
||||
#[must_use]
|
||||
pub fn id(self) -> Entity {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn content(self, content: Content) -> Self {
|
||||
self.model.content.insert(self.id, content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a page and associates it with its parent page.
|
||||
#[allow(clippy::return_self_not_must_use)]
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
pub fn sub_page<P: AutoBind<Message>>(self) -> Self {
|
||||
let sub_page = P::default();
|
||||
|
||||
let page = self.model.info.insert(Info {
|
||||
parent: Some(self.id),
|
||||
..sub_page.info()
|
||||
});
|
||||
|
||||
if let Some(content) = sub_page.content(&mut self.model.sections) {
|
||||
self.model.content.insert(page, content);
|
||||
}
|
||||
|
||||
self.model.page.insert(page, Box::new(sub_page));
|
||||
|
||||
self.model
|
||||
.sub_pages
|
||||
.entry(self.id)
|
||||
.expect("parent page missing")
|
||||
.and_modify(|v| v.push(page))
|
||||
.or_insert_with(|| vec![page]);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
85
page/src/lib.rs
Normal file
85
page/src/lib.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod binder;
|
||||
pub use binder::{AutoBind, Binder};
|
||||
|
||||
mod insert;
|
||||
use downcast_rs::{impl_downcast, Downcast};
|
||||
pub use insert::Insert;
|
||||
|
||||
pub mod section;
|
||||
pub use section::Section;
|
||||
|
||||
use derive_setters::Setters;
|
||||
use slotmap::SlotMap;
|
||||
use std::{borrow::Cow, future::Future, pin::Pin};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// The unique ID of a page.
|
||||
pub struct Entity;
|
||||
}
|
||||
|
||||
/// A collection of sections which a page may be comprised of.
|
||||
pub type Content = Vec<section::Entity>;
|
||||
|
||||
/// A request by a page to run a command in the background.
|
||||
pub type Task<Message> = Pin<Box<dyn Future<Output = Message> + Send>>;
|
||||
|
||||
pub trait Page<Message: 'static>: Downcast {
|
||||
/// Information about the page
|
||||
fn info(&self) -> Info;
|
||||
|
||||
#[must_use]
|
||||
fn content(
|
||||
&self,
|
||||
_sections: &mut SlotMap<section::Entity, Section<Message>>,
|
||||
) -> Option<Content> {
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(unused)]
|
||||
fn load(&self, page: crate::Entity) -> Option<crate::Task<Message>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl_downcast!(Page<Message>);
|
||||
|
||||
/// Information about a page; including its title, icon, and description.
|
||||
#[derive(Setters)]
|
||||
#[must_use]
|
||||
pub struct Info {
|
||||
/// An identifier that is the same between application runs.
|
||||
#[setters(skip)]
|
||||
pub id: Cow<'static, str>,
|
||||
|
||||
/// The icon associated with the page.
|
||||
#[setters(skip)]
|
||||
pub icon_name: Cow<'static, str>,
|
||||
|
||||
/// The title of the page.
|
||||
#[setters(into)]
|
||||
pub title: String,
|
||||
|
||||
/// A description of the page.
|
||||
#[setters(into)]
|
||||
pub description: String,
|
||||
|
||||
/// The parent of the page.
|
||||
#[setters(strip_option)]
|
||||
pub parent: Option<Entity>,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn new(id: impl Into<Cow<'static, str>>, icon_name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
title: String::new(),
|
||||
icon_name: icon_name.into(),
|
||||
id: id.into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
102
page/src/section.rs
Normal file
102
page/src/section.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use derive_setters::Setters;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{Binder, Page};
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// The unique ID of a section of page.
|
||||
pub struct Entity;
|
||||
}
|
||||
|
||||
pub type ViewFn<Message> = Box<
|
||||
dyn for<'a> Fn(
|
||||
&'a Binder<Message>,
|
||||
&'a dyn Page<Message>,
|
||||
&'a Section<Message>,
|
||||
) -> cosmic::Element<'a, Message>,
|
||||
>;
|
||||
|
||||
/// A searchable sub-component of a page.
|
||||
///
|
||||
/// Searches can group multiple sections together.
|
||||
#[derive(Setters)]
|
||||
#[must_use]
|
||||
pub struct Section<Message> {
|
||||
#[setters(into)]
|
||||
pub title: String,
|
||||
pub descriptions: Vec<String>,
|
||||
#[setters(skip)]
|
||||
pub view_fn: ViewFn<Message>,
|
||||
#[setters(bool)]
|
||||
pub search_ignore: bool,
|
||||
}
|
||||
|
||||
impl<Message: 'static> Default for Section<Message> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: String::new(),
|
||||
descriptions: Vec::new(),
|
||||
view_fn: Box::new(unimplemented),
|
||||
search_ignore: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: 'static> Section<Message> {
|
||||
#[must_use]
|
||||
pub fn search_matches(&self, rule: &Regex) -> bool {
|
||||
if self.search_ignore {
|
||||
return false;
|
||||
}
|
||||
|
||||
if rule.is_match(self.title.as_str()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for description in &self.descriptions {
|
||||
if rule.is_match(description.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if the `Model` type does not match the page type.
|
||||
pub fn view<Model: Page<Message>>(
|
||||
mut self,
|
||||
func: impl for<'a> Fn(
|
||||
&'a Binder<Message>,
|
||||
&'a Model,
|
||||
&'a Section<Message>,
|
||||
) -> cosmic::Element<'a, Message>
|
||||
+ 'static,
|
||||
) -> Self {
|
||||
self.view_fn = Box::new(move |binder, model: &dyn Page<Message>, section| {
|
||||
let model = model.downcast_ref::<Model>().unwrap_or_else(|| {
|
||||
panic!(
|
||||
"page model type mismatch: expected {}",
|
||||
std::any::type_name::<Model>()
|
||||
)
|
||||
});
|
||||
|
||||
func(binder, model, section)
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn unimplemented<'a, Message: 'static>(
|
||||
_binder: &'a Binder<Message>,
|
||||
_page: &'a dyn Page<Message>,
|
||||
_section: &'a Section<Message>,
|
||||
) -> cosmic::Element<'a, Message> {
|
||||
cosmic::widget::settings::view_column(vec![cosmic::widget::settings::view_section("").into()])
|
||||
.into()
|
||||
}
|
||||
279
pages/system/Cargo.lock
generated
279
pages/system/Cargo.lock
generated
|
|
@ -1,279 +0,0 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
|
||||
|
||||
[[package]]
|
||||
name = "byte-unit"
|
||||
version = "4.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3348673602e04848647fffaa8e9a861e7b5d5cae6570727b41bde0f722514484"
|
||||
dependencies = [
|
||||
"utf8-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "concat-in-place"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b80dba65d26e0c4b692ad0312b837f1177e8175031af57fd1de4f3bc36b430"
|
||||
|
||||
[[package]]
|
||||
name = "const_format"
|
||||
version = "0.2.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e"
|
||||
dependencies = [
|
||||
"const_format_proc_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const_format_proc_macros"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-settings-system"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"byte-unit",
|
||||
"concat-in-place",
|
||||
"const_format",
|
||||
"memchr",
|
||||
"sysinfo",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "975fe381e0ecba475d4acff52466906d95b153a40324956552e027b2a9eaa89e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name = "cosmic-settings-system"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
@ -10,9 +10,9 @@ license = "MPL-2.0"
|
|||
byte-unit = { version = "4.0.18", default-features = false }
|
||||
const_format = "0.2.30"
|
||||
concat-in-place = "1.1.0"
|
||||
sysinfo = "0.27.7"
|
||||
sysinfo = "0.28.4"
|
||||
memchr = "2.5.0"
|
||||
|
||||
[dependencies.bumpalo]
|
||||
version = "3.12.0"
|
||||
version = "3.12.1"
|
||||
features = ["collections"]
|
||||
|
|
|
|||
210
pages/system/src/about.rs
Normal file
210
pages/system/src/about.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use bumpalo::Bump;
|
||||
use std::{ffi::OsStr, io::Read};
|
||||
use sysinfo::{DiskExt, SystemExt};
|
||||
|
||||
use concat_in_place::strcat;
|
||||
use const_format::concatcp;
|
||||
|
||||
const DMI_DIR: &str = "/sys/devices/virtual/dmi/id/";
|
||||
const BOARD_NAME: &str = concatcp!(DMI_DIR, "board_name");
|
||||
const BOARD_VERSION: &str = concatcp!(DMI_DIR, "board_version");
|
||||
const SYS_VENDOR: &str = concatcp!(DMI_DIR, "sys_vendor");
|
||||
|
||||
const VERSION_IGNORING_PRODUCTS: &[&str] = &["Dev One"];
|
||||
|
||||
#[must_use]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Info {
|
||||
pub desktop_environment: String,
|
||||
pub device_name: String,
|
||||
pub disk_capacity: String,
|
||||
pub graphics: Vec<String>,
|
||||
pub hardware_model: String,
|
||||
pub memory: String,
|
||||
pub operating_system: String,
|
||||
pub os_architecture: String,
|
||||
pub processor: String,
|
||||
pub windowing_system: String,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn load() -> Info {
|
||||
let mut info = Info::default();
|
||||
let mut bump = Bump::with_capacity(8 * 1024);
|
||||
|
||||
architecture(&bump, &mut info.os_architecture);
|
||||
bump.reset();
|
||||
|
||||
hardware_model(&bump, &mut info.hardware_model);
|
||||
bump.reset();
|
||||
|
||||
operating_system(&bump, &mut info.operating_system);
|
||||
bump.reset();
|
||||
|
||||
processor_name(&bump, &mut info.processor);
|
||||
bump.reset();
|
||||
|
||||
let mut sys = sysinfo::System::new();
|
||||
|
||||
sys.refresh_disks_list();
|
||||
sys.refresh_disks();
|
||||
sys.refresh_memory();
|
||||
|
||||
let mut total_capacity = 0;
|
||||
for disk in sys.disks() {
|
||||
total_capacity += disk.total_space();
|
||||
}
|
||||
|
||||
info.disk_capacity = format_size(total_capacity);
|
||||
|
||||
if let Some(name) = sys.host_name() {
|
||||
info.device_name = name;
|
||||
}
|
||||
|
||||
info.memory = format_size(sys.total_memory());
|
||||
|
||||
if let Ok(mut session) = std::env::var("XDG_SESSION_TYPE") {
|
||||
if let Some(first) = session.get_mut(0..1) {
|
||||
first.make_ascii_uppercase();
|
||||
}
|
||||
|
||||
info.windowing_system = session;
|
||||
}
|
||||
|
||||
if let Ok(mut session) = std::env::var("DESKTOP_SESSION") {
|
||||
if let Some(first) = session.get_mut(0..1) {
|
||||
first.make_ascii_uppercase();
|
||||
}
|
||||
|
||||
info.desktop_environment = session;
|
||||
}
|
||||
|
||||
if let Ok(output) = std::process::Command::new("lspci").output() {
|
||||
if let Ok(stdout) = std::str::from_utf8(&output.stdout) {
|
||||
for line in stdout.lines() {
|
||||
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b"VGA") {
|
||||
let line = &line[pos + 3..];
|
||||
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b": ") {
|
||||
info.graphics.push(String::from(&line[pos + 2..]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
pub fn architecture(bump: &Bump, arch: &mut String) {
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(value) = read_to_string("/proc/sys/kernel/arch", buffer) {
|
||||
arch.push_str(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hardware_model(bump: &Bump, hardware_model: &mut String) {
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(mut sys_vendor) = read_to_string(SYS_VENDOR, buffer) {
|
||||
sys_vendor = sys_vendor.trim();
|
||||
|
||||
hardware_model.push_str(sys_vendor);
|
||||
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(mut name) = read_to_string(BOARD_NAME, buffer) {
|
||||
name = name.trim();
|
||||
|
||||
if !name.is_empty() && name != sys_vendor {
|
||||
// Ensure that the name does not contain the vendor.
|
||||
name = match name.strip_prefix(sys_vendor) {
|
||||
Some(stripped) => stripped.trim(),
|
||||
None => name,
|
||||
};
|
||||
|
||||
strcat!(&mut *hardware_model, " " name);
|
||||
}
|
||||
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(mut version) = read_to_string(BOARD_VERSION, buffer) {
|
||||
version = version.trim();
|
||||
|
||||
if !version.is_empty() && !VERSION_IGNORING_PRODUCTS.contains(&name) {
|
||||
strcat!(hardware_model, " (" version.trim() ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operating_system(bump: &Bump, operating_system: &mut String) {
|
||||
let mut buffer = bumpalo::collections::Vec::new_in(bump);
|
||||
let Some(os_release) = read_to_string("/etc/os-release", &mut buffer) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for line in os_release.lines() {
|
||||
if let Some(mut value) = line.strip_prefix("PRETTY_NAME=") {
|
||||
if let Some(v) = value.strip_prefix('"') {
|
||||
value = v;
|
||||
}
|
||||
|
||||
if let Some(v) = value.strip_suffix('"') {
|
||||
value = v;
|
||||
}
|
||||
|
||||
operating_system.push_str(value.trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processor_name(bump: &Bump, name: &mut String) {
|
||||
if let Some(cpuinfo) = read_to_string(
|
||||
"/proc/cpuinfo",
|
||||
&mut bumpalo::collections::Vec::new_in(bump),
|
||||
) {
|
||||
for line in cpuinfo.lines() {
|
||||
if let Some(info) = line.strip_prefix("model name") {
|
||||
if let Some(info) = info.trim_start().strip_prefix(':') {
|
||||
name.push_str(info.trim());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_to_string<'a, P: AsRef<OsStr>>(
|
||||
path: P,
|
||||
buffer: &'a mut bumpalo::collections::Vec<u8>,
|
||||
) -> Option<&'a str> {
|
||||
let mut file = std::fs::File::open(path.as_ref()).ok()?;
|
||||
|
||||
if let Ok(metadata) = file.metadata() {
|
||||
if let Ok(len) = usize::try_from(metadata.len()) {
|
||||
buffer.reserve_exact(len);
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = [0; 16 * 1024];
|
||||
|
||||
loop {
|
||||
match file.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(read) => buffer.extend_from_slice(&buf[..read]),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
std::str::from_utf8(buffer.as_slice()).ok()
|
||||
}
|
||||
|
||||
fn format_size(size: u64) -> String {
|
||||
byte_unit::Byte::from_bytes(size)
|
||||
.get_appropriate_unit(true)
|
||||
.to_string()
|
||||
}
|
||||
|
|
@ -1,208 +1,4 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use bumpalo::Bump;
|
||||
use std::{ffi::OsStr, io::Read};
|
||||
use sysinfo::{DiskExt, SystemExt};
|
||||
|
||||
use concat_in_place::strcat;
|
||||
use const_format::concatcp;
|
||||
|
||||
const DMI_DIR: &str = "/sys/devices/virtual/dmi/id/";
|
||||
const BOARD_NAME: &str = concatcp!(DMI_DIR, "board_name");
|
||||
const BOARD_VERSION: &str = concatcp!(DMI_DIR, "board_version");
|
||||
const SYS_VENDOR: &str = concatcp!(DMI_DIR, "sys_vendor");
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Info {
|
||||
pub desktop_environment: String,
|
||||
pub device_name: String,
|
||||
pub disk_capacity: String,
|
||||
pub graphics: Vec<String>,
|
||||
pub hardware_model: String,
|
||||
pub memory: String,
|
||||
pub operating_system: String,
|
||||
pub os_architecture: String,
|
||||
pub processor: String,
|
||||
pub windowing_system: String,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn load() -> Info {
|
||||
let mut info = Info::default();
|
||||
let mut bump = Bump::with_capacity(8 * 1024);
|
||||
|
||||
architecture(&bump, &mut info.os_architecture);
|
||||
bump.reset();
|
||||
|
||||
hardware_model(&bump, &mut info.hardware_model);
|
||||
bump.reset();
|
||||
|
||||
operating_system(&bump, &mut info.operating_system);
|
||||
bump.reset();
|
||||
|
||||
processor_name(&bump, &mut info.processor);
|
||||
bump.reset();
|
||||
|
||||
let mut sys = sysinfo::System::new();
|
||||
|
||||
sys.refresh_disks_list();
|
||||
sys.refresh_disks();
|
||||
sys.refresh_memory();
|
||||
|
||||
let mut total_capacity = 0;
|
||||
for disk in sys.disks() {
|
||||
total_capacity += disk.total_space();
|
||||
}
|
||||
|
||||
info.disk_capacity = format_size(total_capacity);
|
||||
|
||||
if let Some(name) = sys.host_name() {
|
||||
info.device_name = name;
|
||||
}
|
||||
|
||||
info.memory = format_size(sys.total_memory());
|
||||
|
||||
if let Ok(mut session) = std::env::var("XDG_SESSION_TYPE") {
|
||||
if let Some(first) = session.get_mut(0..1) {
|
||||
first.make_ascii_uppercase();
|
||||
}
|
||||
|
||||
info.windowing_system = session;
|
||||
}
|
||||
|
||||
if let Ok(mut session) = std::env::var("DESKTOP_SESSION") {
|
||||
if let Some(first) = session.get_mut(0..1) {
|
||||
first.make_ascii_uppercase();
|
||||
}
|
||||
|
||||
info.desktop_environment = session;
|
||||
}
|
||||
|
||||
if let Ok(output) = std::process::Command::new("lspci").output() {
|
||||
if let Ok(stdout) = std::str::from_utf8(&output.stdout) {
|
||||
for line in stdout.lines() {
|
||||
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b"VGA") {
|
||||
let line = &line[pos + 3..];
|
||||
if let Some(pos) = memchr::memmem::find(line.as_bytes(), b": ") {
|
||||
info.graphics.push(String::from(&line[pos + 2..]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
pub fn architecture(bump: &Bump, arch: &mut String) {
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(value) = read_to_string("/proc/sys/kernel/arch", buffer) {
|
||||
arch.push_str(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hardware_model(bump: &Bump, hardware_model: &mut String) {
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(mut sys_vendor) = read_to_string(SYS_VENDOR, buffer) {
|
||||
sys_vendor = sys_vendor.trim();
|
||||
|
||||
hardware_model.push_str(sys_vendor);
|
||||
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(mut name) = read_to_string(BOARD_NAME, buffer) {
|
||||
name = name.trim();
|
||||
|
||||
if !name.is_empty() && name != sys_vendor {
|
||||
// Ensure that the name does not contain the vendor.
|
||||
name = match name.strip_prefix(sys_vendor) {
|
||||
Some(stripped) => stripped.trim(),
|
||||
None => name,
|
||||
};
|
||||
|
||||
strcat!(&mut *hardware_model, " " name);
|
||||
}
|
||||
|
||||
let buffer = &mut bumpalo::collections::Vec::new_in(bump);
|
||||
if let Some(mut version) = read_to_string(BOARD_VERSION, buffer) {
|
||||
version = version.trim();
|
||||
|
||||
// These have bogus values for their version.
|
||||
const IGNORE_PRODUCTS: &[&str] = &["Dev One"];
|
||||
|
||||
if !version.is_empty() && !IGNORE_PRODUCTS.contains(&name) {
|
||||
strcat!(hardware_model, " (" version.trim() ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operating_system(bump: &Bump, operating_system: &mut String) {
|
||||
let mut buffer = bumpalo::collections::Vec::new_in(bump);
|
||||
let Some(os_release) = read_to_string("/etc/os-release", &mut buffer) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for line in os_release.lines() {
|
||||
if let Some(mut value) = line.strip_prefix("PRETTY_NAME=") {
|
||||
if let Some(v) = value.strip_prefix('"') {
|
||||
value = v;
|
||||
}
|
||||
|
||||
if let Some(v) = value.strip_suffix('"') {
|
||||
value = v;
|
||||
}
|
||||
|
||||
operating_system.push_str(value.trim());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processor_name(bump: &Bump, name: &mut String) {
|
||||
if let Some(cpuinfo) = read_to_string(
|
||||
"/proc/cpuinfo",
|
||||
&mut bumpalo::collections::Vec::new_in(bump),
|
||||
) {
|
||||
for line in cpuinfo.lines() {
|
||||
if let Some(info) = line.strip_prefix("model name") {
|
||||
if let Some(info) = info.trim_start().strip_prefix(":") {
|
||||
name.push_str(info.trim());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_to_string<'a, P: AsRef<OsStr>>(
|
||||
path: P,
|
||||
buffer: &'a mut bumpalo::collections::Vec<u8>,
|
||||
) -> Option<&'a str> {
|
||||
let mut file = std::fs::File::open(path.as_ref()).ok()?;
|
||||
|
||||
if let Ok(metadata) = file.metadata() {
|
||||
buffer.reserve_exact(metadata.len() as usize);
|
||||
}
|
||||
|
||||
let mut buf = [0; 16 * 1024];
|
||||
|
||||
loop {
|
||||
match file.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(read) => buffer.extend_from_slice(&buf[..read]),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
std::str::from_utf8(buffer.as_slice()).ok()
|
||||
}
|
||||
|
||||
fn format_size(size: u64) -> String {
|
||||
byte_unit::Byte::from_bytes(size)
|
||||
.get_appropriate_unit(true)
|
||||
.to_string()
|
||||
}
|
||||
pub mod about;
|
||||
|
|
|
|||
14
pages/time/Cargo.toml
Normal file
14
pages/time/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "cosmic-settings-time"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
icu_calendar = "1.2.0"
|
||||
icu_timezone = "1.2.0"
|
||||
timedate-zbus = "0.1.0"
|
||||
|
||||
[dependencies.zbus]
|
||||
version = "3.12.0"
|
||||
default-features = false
|
||||
features = ["tokio"]
|
||||
48
pages/time/src/lib.rs
Normal file
48
pages/time/src/lib.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use icu_calendar::{types::IsoSecond, DateTime, Iso};
|
||||
use icu_timezone::CustomTimeZone;
|
||||
use timedate_zbus::TimeDateProxy;
|
||||
|
||||
pub struct Info {
|
||||
pub can_ntp: bool,
|
||||
pub timezone: CustomTimeZone,
|
||||
pub local_time: DateTime<Iso>,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub async fn load(proxy: &TimeDateProxy<'_>) -> Option<Info> {
|
||||
let can_ntp = proxy.can_ntp().await.unwrap_or_default();
|
||||
|
||||
let Ok(timezone) = proxy.timezone()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.parse::<CustomTimeZone>() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(duration) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let seconds = duration.as_secs();
|
||||
|
||||
let Ok(iso_seconds) = IsoSecond::try_from((seconds % 60) as u8) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let mut local_time = DateTime::from_minutes_since_local_unix_epoch((seconds / 60) as i32);
|
||||
|
||||
local_time.time.second = iso_seconds;
|
||||
|
||||
Some(Info {
|
||||
can_ntp,
|
||||
timezone,
|
||||
local_time,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("appearance", "preferences-pop-desktop-appearance-symbolic")
|
||||
.title(fl!("appearance"))
|
||||
.description(fl!("appearance", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(Section::new())])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("dock", "preferences-pop-desktop-dock-symbolic")
|
||||
.title(fl!("dock"))
|
||||
.description(fl!("dock", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(Section::new())])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("notifications", "preferences-system-notifications-symbolic")
|
||||
.title(fl!("notifications"))
|
||||
.description(fl!("notifications", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(Section::new())])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod desktop;
|
||||
pub mod networking;
|
||||
pub mod section;
|
||||
pub mod time;
|
||||
|
||||
pub use section::Section;
|
||||
pub mod sound;
|
||||
pub mod system;
|
||||
|
||||
mod model;
|
||||
|
||||
pub use model::{Insert, Model, PageTask};
|
||||
|
||||
use derive_setters::Setters;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// ID of a page
|
||||
pub struct Entity;
|
||||
}
|
||||
|
||||
pub trait Page {
|
||||
type Model: Default + 'static;
|
||||
|
||||
fn page() -> Meta;
|
||||
|
||||
#[must_use]
|
||||
fn content(_sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Attaches sub-pages to the page.
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
fn sub_pages(page: Insert) -> Insert {
|
||||
page
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(unused)]
|
||||
fn load(page: Entity) -> PageTask {
|
||||
Box::pin(async move { crate::Message::None })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Setters)]
|
||||
#[must_use]
|
||||
pub struct Meta {
|
||||
/// A unique identity that is the same between application runs.
|
||||
#[setters(skip)]
|
||||
pub id: &'static str,
|
||||
|
||||
/// The icon associated with the page.
|
||||
#[setters(skip)]
|
||||
pub icon_name: &'static str,
|
||||
|
||||
/// The title of the page.
|
||||
#[setters(into)]
|
||||
pub title: String,
|
||||
|
||||
/// A description of the page.
|
||||
#[setters(into)]
|
||||
pub description: String,
|
||||
|
||||
/// The parent of the page.
|
||||
#[setters(strip_option)]
|
||||
pub parent: Option<Entity>,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub const fn new(id: &'static str, icon_name: &'static str) -> Self {
|
||||
Self {
|
||||
title: String::new(),
|
||||
icon_name,
|
||||
id,
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Content = Vec<section::Entity>;
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use crate::page::{self, section, Content, Meta, Page, Section};
|
||||
use cosmic::iced_native::command::{Action, Command};
|
||||
use regex::Regex;
|
||||
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
|
||||
|
||||
pub type PageTask = Pin<Box<dyn Future<Output = crate::Message> + Send>>;
|
||||
|
||||
pub struct Model {
|
||||
pub pages: SlotMap<page::Entity, Meta>,
|
||||
pub page_load: SecondaryMap<page::Entity, fn(page::Entity) -> PageTask>,
|
||||
pub resource: HashMap<TypeId, Box<dyn Any>>,
|
||||
pub storage: HashMap<TypeId, SecondaryMap<page::Entity, Box<dyn Any>>>,
|
||||
pub sub_pages: SparseSecondaryMap<page::Entity, Vec<page::Entity>>,
|
||||
pub sections: SlotMap<section::Entity, Section>,
|
||||
pub content: SparseSecondaryMap<page::Entity, Content>,
|
||||
}
|
||||
|
||||
impl Default for Model {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: SparseSecondaryMap::new(),
|
||||
pages: SlotMap::with_key(),
|
||||
page_load: SecondaryMap::new(),
|
||||
resource: HashMap::new(),
|
||||
sections: SlotMap::with_key(),
|
||||
storage: HashMap::new(),
|
||||
sub_pages: SparseSecondaryMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Check if a page exists in the model.
|
||||
#[must_use]
|
||||
pub fn contains_item(&self, id: page::Entity) -> bool {
|
||||
self.pages.contains_key(id)
|
||||
}
|
||||
|
||||
/// Returns the content of a page, if it has any.
|
||||
#[must_use]
|
||||
pub fn content(&self, page: page::Entity) -> Option<&[section::Entity]> {
|
||||
self.content.get(page).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Get an immutable reference to data associated with a page.
|
||||
#[must_use]
|
||||
pub fn data<Data: 'static>(&self, id: page::Entity) -> Option<&Data> {
|
||||
self.storage
|
||||
.get(&TypeId::of::<Data>())
|
||||
.and_then(|storage| storage.get(id))
|
||||
.and_then(|data| data.downcast_ref())
|
||||
}
|
||||
|
||||
/// Get a mutable reference to data associated with a page.
|
||||
pub fn data_mut<Data: 'static>(&mut self, id: page::Entity) -> Option<&mut Data> {
|
||||
self.storage
|
||||
.get_mut(&TypeId::of::<Data>())
|
||||
.and_then(|storage| storage.get_mut(id))
|
||||
.and_then(|data| data.downcast_mut())
|
||||
}
|
||||
|
||||
/// Associates data with the item.
|
||||
pub fn data_set<Data: 'static>(&mut self, id: page::Entity, data: Data) {
|
||||
if self.contains_item(id) {
|
||||
self.storage
|
||||
.entry(TypeId::of::<Data>())
|
||||
.or_insert_with(SecondaryMap::new)
|
||||
.insert(id, Box::new(data));
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a specific data type from the item.
|
||||
pub fn data_remove<Data: 'static>(&mut self, id: page::Entity) {
|
||||
self.storage
|
||||
.get_mut(&TypeId::of::<Data>())
|
||||
.and_then(|storage| storage.remove(id));
|
||||
}
|
||||
|
||||
pub fn init_page(&mut self, id: page::Entity) -> Option<Command<crate::Message>> {
|
||||
if let Some(func) = self.page_load.get(id).copied() {
|
||||
return Some(Command::single(Action::Future(func(id))));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Registers a new page in the settings panel.
|
||||
pub fn register<P: Page>(&mut self) -> Insert {
|
||||
let id = self.pages.insert(P::page());
|
||||
|
||||
self.page_load.insert(id, P::load);
|
||||
|
||||
if let Some(content) = P::content(&mut self.sections) {
|
||||
self.content.insert(id, content);
|
||||
}
|
||||
|
||||
self.resource_register::<P::Model>();
|
||||
|
||||
P::sub_pages(Insert { id, model: self })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resource<Resource: 'static>(&self) -> Option<&Resource> {
|
||||
self.resource
|
||||
.get(&TypeId::of::<Resource>())
|
||||
.and_then(|resource| resource.downcast_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resource_mut<Resource: 'static>(&mut self) -> Option<&mut Resource> {
|
||||
self.resource
|
||||
.get_mut(&TypeId::of::<Resource>())
|
||||
.and_then(|resource| resource.downcast_mut())
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
pub fn resource_register<Resource: Default + 'static>(&mut self) {
|
||||
self.resource
|
||||
.entry(TypeId::of::<Resource>())
|
||||
.or_insert_with(|| Box::new(Resource::default()));
|
||||
}
|
||||
|
||||
/// Finds content of panels that match the search.
|
||||
pub fn search<'a>(
|
||||
&'a self,
|
||||
rule: &'a Regex,
|
||||
) -> impl Iterator<Item = (page::Entity, section::Entity)> + 'a {
|
||||
generator::Gn::new_scoped_local(|mut s| {
|
||||
for (page, sections) in self.content.iter() {
|
||||
for id in sections {
|
||||
if self.sections[*id].matches_search(rule) {
|
||||
s.yield_((page, *id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generator::done!();
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the sub-pages of a page, if it has any.
|
||||
pub fn sub_pages(&self, page: page::Entity) -> Option<&[page::Entity]> {
|
||||
self.sub_pages.get(page).map(AsRef::as_ref)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Insert<'a> {
|
||||
pub model: &'a mut Model,
|
||||
pub id: page::Entity,
|
||||
}
|
||||
|
||||
impl<'a> Insert<'a> {
|
||||
#[must_use]
|
||||
pub fn id(self) -> page::Entity {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn content(self, content: Content) -> Self {
|
||||
self.model.content.insert(self.id, content);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a page and associates it with its parent page.
|
||||
#[allow(clippy::return_self_not_must_use)]
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
pub fn sub_page<P: Page>(self) -> Self {
|
||||
let page = self.model.pages.insert(Meta {
|
||||
parent: Some(self.id),
|
||||
..P::page()
|
||||
});
|
||||
|
||||
self.model.page_load.insert(page, P::load);
|
||||
|
||||
if let Some(content) = P::content(&mut self.model.sections) {
|
||||
self.model.content.insert(page, content);
|
||||
}
|
||||
|
||||
self.model.resource_register::<P::Model>();
|
||||
|
||||
self.model
|
||||
.sub_pages
|
||||
.entry(self.id)
|
||||
.expect("parent page missing")
|
||||
.and_modify(|v| v.push(page))
|
||||
.or_insert_with(|| vec![page]);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use derive_setters::Setters;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::SettingsApp;
|
||||
|
||||
slotmap::new_key_type! {
|
||||
/// ID of a page section
|
||||
pub struct Entity;
|
||||
}
|
||||
|
||||
/// A searchable sub-component of a page. Searches can group multiple sections together.
|
||||
#[derive(Setters)]
|
||||
#[must_use]
|
||||
pub struct Section {
|
||||
#[setters(into)]
|
||||
pub title: String,
|
||||
pub descriptions: Vec<String>,
|
||||
pub view_fn: for<'a> fn(&'a SettingsApp, &'a Section) -> cosmic::Element<'a, crate::Message>,
|
||||
#[setters(bool)]
|
||||
pub search_ignore: bool,
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
title: String::new(),
|
||||
descriptions: Vec::new(),
|
||||
view_fn: Self::unimplemented,
|
||||
search_ignore: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn matches_search(&self, rule: &Regex) -> bool {
|
||||
if self.search_ignore {
|
||||
return false;
|
||||
}
|
||||
|
||||
if rule.is_match(self.title.as_str()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for description in &self.descriptions {
|
||||
if rule.is_match(description.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn unimplemented<'a>(
|
||||
_app: &'a SettingsApp,
|
||||
_section: &'a Section,
|
||||
) -> cosmic::Element<'a, crate::Message> {
|
||||
cosmic::widget::settings::view_column(vec![cosmic::widget::settings::view_section("")
|
||||
.add(crate::widget::unimplemented_page())
|
||||
.into()])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
page::{self, desktop::wallpaper::settings, Content, Section},
|
||||
SettingsApp,
|
||||
};
|
||||
use cosmic::{
|
||||
iced::{
|
||||
widget::{horizontal_space, row},
|
||||
Length,
|
||||
},
|
||||
widget::{icon, list_column, settings, text},
|
||||
};
|
||||
use cosmic_settings_system::Info;
|
||||
use slotmap::SlotMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Info(Info),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Model {
|
||||
info: Info,
|
||||
support_page: page::Entity,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::Info(info) => self.info = info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("about", "help-about-symbolic")
|
||||
.title(fl!("about"))
|
||||
.description(fl!("about", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<page::section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![
|
||||
sections.insert(distributor_logo()),
|
||||
sections.insert(device()),
|
||||
sections.insert(hardware()),
|
||||
sections.insert(os()),
|
||||
sections.insert(related()),
|
||||
])
|
||||
}
|
||||
|
||||
fn load(_page: page::Entity) -> crate::page::PageTask {
|
||||
Box::pin(async move { crate::Message::About(Message::Info(Info::load())) })
|
||||
}
|
||||
}
|
||||
|
||||
fn device() -> Section {
|
||||
Section::new()
|
||||
.descriptions(vec![fl!("about-device"), fl!("about-device", "desc")])
|
||||
.view_fn(|app, section| {
|
||||
let desc = §ion.descriptions;
|
||||
let model = model(app);
|
||||
|
||||
let device_name = settings::item::builder(&desc[0])
|
||||
.description(&desc[1])
|
||||
.control(text(&model.info.device_name));
|
||||
|
||||
list_column().add(device_name).into()
|
||||
})
|
||||
}
|
||||
|
||||
fn distributor_logo() -> Section {
|
||||
Section::new().search_ignore().view_fn(|_app, _section| {
|
||||
row!(
|
||||
horizontal_space(Length::Fill),
|
||||
icon("distributor-logo", 78),
|
||||
horizontal_space(Length::Fill),
|
||||
)
|
||||
// Add extra padding to reach 40px from the first section.
|
||||
.padding([0, 16, 0, 16])
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn hardware() -> Section {
|
||||
Section::new()
|
||||
.title(fl!("about-hardware"))
|
||||
.descriptions(vec![
|
||||
fl!("about-hardware", "model"),
|
||||
fl!("about-hardware", "memory"),
|
||||
fl!("about-hardware", "processor"),
|
||||
fl!("about-hardware", "graphics"),
|
||||
fl!("about-hardware", "disk-capacity"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let desc = §ion.descriptions;
|
||||
let model = model(app);
|
||||
|
||||
let mut sections = settings::view_section(§ion.title)
|
||||
.add(settings::item(&desc[0], text(&model.info.hardware_model)))
|
||||
.add(settings::item(&desc[1], text(&model.info.memory)))
|
||||
.add(settings::item(&desc[2], text(&model.info.processor)));
|
||||
|
||||
for card in &model.info.graphics {
|
||||
sections = sections.add(settings::item(&desc[3], text(card.as_str())));
|
||||
}
|
||||
|
||||
// .add(settings::item(&desc[3], text(&model.info.graphics)))
|
||||
sections
|
||||
.add(settings::item(&desc[4], text(&model.info.disk_capacity)))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn os() -> Section {
|
||||
Section::new()
|
||||
.title(fl!("about-os"))
|
||||
.descriptions(vec![
|
||||
fl!("about-os", "os"),
|
||||
fl!("about-os", "os-architecture"),
|
||||
fl!("about-os", "desktop-environment"),
|
||||
fl!("about-os", "windowing-system"),
|
||||
])
|
||||
.view_fn(|app, section| {
|
||||
let model = model(app);
|
||||
|
||||
let desc = §ion.descriptions;
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(&desc[0], text(&model.info.operating_system)))
|
||||
.add(settings::item(&desc[1], text(&model.info.os_architecture)))
|
||||
.add(settings::item(
|
||||
&desc[2],
|
||||
text(&model.info.desktop_environment),
|
||||
))
|
||||
.add(settings::item(&desc[3], text(&model.info.windowing_system)))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn related() -> Section {
|
||||
Section::new()
|
||||
.title(fl!("about-related"))
|
||||
.descriptions(vec![fl!("about-related", "support")])
|
||||
.view_fn(|_app, section| {
|
||||
settings::view_section(§ion.title)
|
||||
.add(settings::item(§ion.descriptions[0], text("TODO")))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn model(app: &SettingsApp) -> &Model {
|
||||
app.pages
|
||||
.resource::<Model>()
|
||||
.expect("missing system->about model")
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("firmware", "firmware-manager-symbolic")
|
||||
.title(fl!("firmware"))
|
||||
.description(fl!("firmware", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(Section::new())])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod about;
|
||||
pub mod firmware;
|
||||
pub mod users;
|
||||
|
||||
use crate::page;
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("system", "system-users-symbolic").title(fl!("system"))
|
||||
}
|
||||
|
||||
fn sub_pages(page: page::Insert) -> page::Insert {
|
||||
page.sub_page::<users::Page>()
|
||||
.sub_page::<about::Page>()
|
||||
.sub_page::<firmware::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Model {}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = super::Model;
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("users", "system-users-symbolic")
|
||||
.title(fl!("users"))
|
||||
.description(fl!("users", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(Section::new())])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page;
|
||||
|
||||
pub mod date;
|
||||
pub mod region;
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = ();
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("time", "preferences-system-time-symbolic")
|
||||
.title(fl!("time"))
|
||||
.description(fl!("time", "desc"))
|
||||
}
|
||||
|
||||
fn sub_pages(page: page::Insert) -> page::Insert {
|
||||
page.sub_page::<date::Page>().sub_page::<region::Page>()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::page::{self, section, Content, Section};
|
||||
use slotmap::SlotMap;
|
||||
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page for Page {
|
||||
type Model = ();
|
||||
|
||||
fn page() -> page::Meta {
|
||||
page::Meta::new("time-region", "preferences-desktop-locale-symbolic")
|
||||
.title(fl!("time-region"))
|
||||
.description(fl!("time-region", "desc"))
|
||||
}
|
||||
|
||||
fn content(sections: &mut SlotMap<section::Entity, Section>) -> Option<Content> {
|
||||
Some(vec![sections.insert(Section::new())])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue