Align recording dialog behavior and shell theme styling
This commit is contained in:
1 parent
24e15f76cb
commit
8832a9bf2e
9 files changed
+263
-26
No files matched your search
@@ -47,6 +47,32 @@ pub fn foreground(background: Color, preferred: Color) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prefer the theme's foreground for filled controls. A small shade adjustment
|
||||||
|
/// is less disruptive than flipping light text to black when selecting a row.
|
||||||
|
/// Very bright/dark accents still use a contrasting foreground rather than
|
||||||
|
/// changing the user's accent into a substantially different color.
|
||||||
|
pub fn accent_pair(background: Color, preferred: Color) -> (Color, Color) {
|
||||||
|
let on = composite(preferred, background);
|
||||||
|
let shade = if luminance(on) > luminance(background) {
|
||||||
|
Color::BLACK
|
||||||
|
} else {
|
||||||
|
Color::WHITE
|
||||||
|
};
|
||||||
|
for step in 0..=20 {
|
||||||
|
let fill = composite(
|
||||||
|
Color {
|
||||||
|
a: step as f32 / 100.,
|
||||||
|
..shade
|
||||||
|
},
|
||||||
|
background,
|
||||||
|
);
|
||||||
|
if contrast(fill, on) >= 4.5 {
|
||||||
|
return (fill, on);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(background, foreground(background, preferred))
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve configured accent transparency before passing an opaque color to a toolkit.
|
/// Resolve configured accent transparency before passing an opaque color to a toolkit.
|
||||||
pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::palette::Srgb {
|
pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::palette::Srgb {
|
||||||
rgba(composite(accent, surface)).color
|
rgba(composite(accent, surface)).color
|
||||||
@@ -56,8 +82,7 @@ pub fn accent_color(accent: Color, surface: Color) -> cosmic::cosmic_theme::pale
|
|||||||
pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
|
pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
|
||||||
let surface: Color = native.primary(false).base.into();
|
let surface: Color = native.primary(false).base.into();
|
||||||
for component in [&mut native.accent, &mut native.accent_button] {
|
for component in [&mut native.accent, &mut native.accent_button] {
|
||||||
let base = composite(component.base.into(), surface);
|
let (base, on) = accent_pair(composite(component.base.into(), surface), preferred);
|
||||||
let on = foreground(base, preferred);
|
|
||||||
component.hover = rgba(readable_fill(
|
component.hover = rgba(readable_fill(
|
||||||
composite(component.hover.into(), surface),
|
composite(component.hover.into(), surface),
|
||||||
base,
|
base,
|
||||||
@@ -68,7 +93,11 @@ pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
|
|||||||
base,
|
base,
|
||||||
on,
|
on,
|
||||||
));
|
));
|
||||||
component.selected = rgba(composite(component.selected.into(), surface));
|
component.selected = rgba(readable_fill(
|
||||||
|
composite(component.selected.into(), surface),
|
||||||
|
base,
|
||||||
|
on,
|
||||||
|
));
|
||||||
component.base = rgba(base);
|
component.base = rgba(base);
|
||||||
component.on = rgba(on);
|
component.on = rgba(on);
|
||||||
component.selected_text = rgba(foreground(
|
component.selected_text = rgba(foreground(
|
||||||
@@ -170,7 +199,7 @@ mod tests {
|
|||||||
for surface in [Color::BLACK, Color::WHITE] {
|
for surface in [Color::BLACK, Color::WHITE] {
|
||||||
for alpha in [0., 0.2, 0.5, 1.] {
|
for alpha in [0., 0.2, 0.5, 1.] {
|
||||||
let fill = composite(Color::from_rgba8(r, g, b, alpha), surface);
|
let fill = composite(Color::from_rgba8(r, g, b, alpha), surface);
|
||||||
let on = foreground(fill, Color::from_rgb8(120, 130, 140));
|
let (fill, on) = accent_pair(fill, Color::from_rgb8(120, 130, 140));
|
||||||
assert!(contrast(fill, on) >= 4.5, "{fill:?}: {on:?}");
|
assert!(contrast(fill, on) >= 4.5, "{fill:?}: {on:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +208,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
|
fn blue_accent_preserves_light_theme_text_in_every_state() {
|
||||||
|
let preferred = Color::from_rgb8(244, 247, 251);
|
||||||
|
let mut native = cosmic::cosmic_theme::ThemeBuilder::dark()
|
||||||
|
.accent(rgba(Color::from_rgb8(61, 123, 230)).color)
|
||||||
|
.build();
|
||||||
|
apply(&mut native, preferred);
|
||||||
|
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||||
|
for state in [
|
||||||
|
State::Active,
|
||||||
|
State::Hovered,
|
||||||
|
State::Pressed,
|
||||||
|
State::Disabled,
|
||||||
|
] {
|
||||||
|
let actual = style(&theme, state, false);
|
||||||
|
assert_eq!(actual.text_color, Some(preferred));
|
||||||
|
assert_eq!(actual.icon_color, Some(preferred));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
fn native_button_states_keep_text_and_icons_readable() {
|
fn native_button_states_keep_text_and_icons_readable() {
|
||||||
for accent in [
|
for accent in [
|
||||||
Color::BLACK,
|
Color::BLACK,
|
||||||
|
|||||||
@@ -146,8 +146,9 @@ these six visually distinct.
|
|||||||
| `shadow` | `"#00000055"` | Window shadow color |
|
| `shadow` | `"#00000055"` | Window shadow color |
|
||||||
|
|
||||||
Ferese derives the text and icon color for filled accent controls from the
|
Ferese derives the text and icon color for filled accent controls from the
|
||||||
configured theme. It keeps the configured text color when it reaches a 4.5:1
|
configured theme. It keeps the configured text color and, when necessary, shades
|
||||||
contrast ratio; otherwise it chooses black or white. The same rule is shared by
|
the accent fill slightly to reach a 4.5:1 contrast ratio. If that would require a
|
||||||
|
large color change, it chooses black or white instead. The same rule is shared by
|
||||||
native dialogs, settings selections, and notification badges. Hover, pressed,
|
native dialogs, settings selections, and notification badges. Hover, pressed,
|
||||||
and disabled accent-button states use the same policy. Small filled controls
|
and disabled accent-button states use the same policy. Small filled controls
|
||||||
resolve accent transparency against their theme surface so wallpaper cannot
|
resolve accent transparency against their theme surface so wallpaper cannot
|
||||||
|
|||||||
@@ -27,10 +27,15 @@ may remain at the path shown in the error.
|
|||||||
Use a Wayland screen-sharing or recording source in your application. Ferese
|
Use a Wayland screen-sharing or recording source in your application. Ferese
|
||||||
opens a display picker. Select a display and press **Share**. Nothing is selected
|
opens a display picker. Select a display and press **Share**. Nothing is selected
|
||||||
by default. Applications that request multiple displays can share up to four.
|
by default. Applications that request multiple displays can share up to four.
|
||||||
The picker uses your Ferese theme colors, font, and shell corner radius.
|
The picker uses your Ferese theme colors, font, and shell corner radius. Its
|
||||||
|
background follows `theme.material.style`: opaque in solid mode, or translucent
|
||||||
|
using `theme.surface.popover.opacity`. Text and icons remain opaque.
|
||||||
|
|
||||||
A **screen sharing** window remains visible while sharing. Press **Stop sharing**
|
Other applications keep a **screen sharing** window with a **Stop sharing** button.
|
||||||
or close that window to revoke the session. Closing the requesting application,
|
Ferese’s built-in recorder uses the top-bar stop control instead, leaving your
|
||||||
|
windows accessible after display selection. The command-line recorder keeps the
|
||||||
|
sharing window when it is not launched by the shell. Press **Stop sharing** or
|
||||||
|
close the sharing window to revoke an ordinary application’s session. Closing the requesting application,
|
||||||
locking Ferese, disconnecting the selected display, or changing its capture size
|
locking Ferese, disconnecting the selected display, or changing its capture size
|
||||||
also ends sharing. A new request needs fresh consent; selections are not saved.
|
also ends sharing. A new request needs fresh consent; selections are not saved.
|
||||||
Everything visible on a shared display, including notifications, is included.
|
Everything visible on a shared display, including notifications, is included.
|
||||||
|
|||||||
+113
-8
@@ -73,12 +73,80 @@ struct Session {
|
|||||||
phase: Phase,
|
phase: Phase,
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
cursor: bool,
|
cursor: bool,
|
||||||
|
bar_controlled: bool,
|
||||||
cancel: Arc<Cancel>,
|
cancel: Arc<Cancel>,
|
||||||
}
|
}
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
sessions: Arc<Mutex<HashMap<String, Session>>>,
|
sessions: Arc<Mutex<HashMap<String, Session>>>,
|
||||||
}
|
}
|
||||||
|
// This private interface only transfers stop controls to the shell's own recorder.
|
||||||
|
// The ordinary portal picker and all session revocation rules still apply.
|
||||||
|
struct RecorderControl(Backend);
|
||||||
|
#[zbus::interface(name = "org.ferese.ScreenRecorder")]
|
||||||
|
impl RecorderControl {
|
||||||
|
async fn use_bar_controls(
|
||||||
|
&self,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<()> {
|
||||||
|
let sender = header
|
||||||
|
.sender()
|
||||||
|
.ok_or_else(|| error("Missing recorder caller"))?;
|
||||||
|
if !owns_session(sender.as_str(), session_handle.as_str()) {
|
||||||
|
return Err(zbus::fdo::Error::AccessDenied(
|
||||||
|
"Not your recording session".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let dbus = zbus::fdo::DBusProxy::new(connection).await?;
|
||||||
|
let pid = dbus
|
||||||
|
.get_connection_unix_process_id(sender.clone().into())
|
||||||
|
.await?;
|
||||||
|
if !shell_recorder(pid) {
|
||||||
|
return Err(zbus::fdo::Error::AccessDenied(
|
||||||
|
"The shell must own the recording controls".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut sessions = self.0.sessions.lock().await;
|
||||||
|
let session = sessions
|
||||||
|
.get_mut(session_handle.as_str())
|
||||||
|
.ok_or_else(|| error("Unknown session"))?;
|
||||||
|
if session.phase != Phase::Created {
|
||||||
|
return Err(error("Recording already configured"));
|
||||||
|
}
|
||||||
|
session.bar_controlled = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn owns_session(sender: &str, path: &str) -> bool {
|
||||||
|
let Some(sender) = sender.strip_prefix(':') else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
path.starts_with(&format!("{PATH}/session/{}/", sender.replace('.', "_")))
|
||||||
|
}
|
||||||
|
fn shell_recorder(pid: u32) -> bool {
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
let matches_binary = |pid: u32, name: &str| -> Option<bool> {
|
||||||
|
let expected = std::env::current_exe().ok()?.parent()?.join(name);
|
||||||
|
let expected = std::fs::metadata(expected).ok()?;
|
||||||
|
let actual = std::fs::metadata(format!("/proc/{pid}/exe")).ok()?;
|
||||||
|
Some(expected.dev() == actual.dev() && expected.ino() == actual.ino())
|
||||||
|
};
|
||||||
|
if matches_binary(pid, "ferese-record") != Some(true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let parent = std::fs::read_to_string(format!("/proc/{pid}/status"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|status| {
|
||||||
|
status.lines().find_map(|line| {
|
||||||
|
line.strip_prefix("PPid:")
|
||||||
|
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
parent.is_some_and(|pid| matches_binary(pid, "ferese-shell") == Some(true))
|
||||||
|
}
|
||||||
|
|
||||||
async fn authorize(connection: &Connection, header: &Header<'_>) -> zbus::fdo::Result<String> {
|
async fn authorize(connection: &Connection, header: &Header<'_>) -> zbus::fdo::Result<String> {
|
||||||
let sender = header
|
let sender = header
|
||||||
.sender()
|
.sender()
|
||||||
@@ -199,6 +267,7 @@ impl Backend {
|
|||||||
phase: Phase::Created,
|
phase: Phase::Created,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
cursor: false,
|
cursor: false,
|
||||||
|
bar_controlled: false,
|
||||||
cancel: Arc::default(),
|
cancel: Arc::default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -257,7 +326,7 @@ impl Backend {
|
|||||||
{
|
{
|
||||||
return Err(error("Invalid request path"));
|
return Err(error("Invalid request path"));
|
||||||
}
|
}
|
||||||
let (multiple, cursor, cancel) = {
|
let (multiple, cursor, bar_controlled, cancel) = {
|
||||||
let mut sessions = self.sessions.lock().await;
|
let mut sessions = self.sessions.lock().await;
|
||||||
let session = sessions
|
let session = sessions
|
||||||
.get_mut(session_handle.as_str())
|
.get_mut(session_handle.as_str())
|
||||||
@@ -266,7 +335,12 @@ impl Backend {
|
|||||||
return Err(error("Select sources before starting a session"));
|
return Err(error("Select sources before starting a session"));
|
||||||
}
|
}
|
||||||
session.phase = Phase::Starting;
|
session.phase = Phase::Starting;
|
||||||
(session.multiple, session.cursor, session.cancel.clone())
|
(
|
||||||
|
session.multiple,
|
||||||
|
session.cursor,
|
||||||
|
session.bar_controlled,
|
||||||
|
session.cancel.clone(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
let inserted = connection
|
let inserted = connection
|
||||||
.object_server()
|
.object_server()
|
||||||
@@ -290,7 +364,7 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
let result = tokio::select! {
|
let result = tokio::select! {
|
||||||
_ = cancel.wait() => Err(StartError::Cancelled),
|
_ = cancel.wait() => Err(StartError::Cancelled),
|
||||||
result = tokio::time::timeout(Duration::from_secs(120), start_streams(&app_id, multiple, cursor)) => result.unwrap_or_else(|_| Err(StartError::Failed("Sharing request timed out".into()))),
|
result = tokio::time::timeout(Duration::from_secs(120), start_streams(if bar_controlled { "Ferese" } else { &app_id }, multiple, cursor, !bar_controlled)) => result.unwrap_or_else(|_| Err(StartError::Failed("Sharing request timed out".into()))),
|
||||||
};
|
};
|
||||||
let _ = connection
|
let _ = connection
|
||||||
.object_server()
|
.object_server()
|
||||||
@@ -414,7 +488,8 @@ async fn start_streams(
|
|||||||
app: &str,
|
app: &str,
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
cursor: bool,
|
cursor: bool,
|
||||||
) -> Result<(Vec<Child>, Child, Vec<Ready>), StartError> {
|
show_indicator: bool,
|
||||||
|
) -> Result<(Vec<Child>, Option<Child>, Vec<Ready>), StartError> {
|
||||||
let sources = tokio::task::spawn_blocking(|| {
|
let sources = tokio::task::spawn_blocking(|| {
|
||||||
Capture::connect(&AtomicBool::new(false)).map(|capture| capture.sources())
|
Capture::connect(&AtomicBool::new(false)).map(|capture| capture.sources())
|
||||||
})
|
})
|
||||||
@@ -469,13 +544,19 @@ async fn start_streams(
|
|||||||
streams.push(ready);
|
streams.push(ready);
|
||||||
children.push(child);
|
children.push(child);
|
||||||
}
|
}
|
||||||
let indicator = picker(&Prompt {
|
let indicator = if show_indicator {
|
||||||
|
Some(
|
||||||
|
picker(&Prompt {
|
||||||
app: app.to_owned(),
|
app: app.to_owned(),
|
||||||
sources: selected,
|
sources: selected,
|
||||||
multiple,
|
multiple,
|
||||||
indicator: true,
|
indicator: true,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
Ok((children, indicator, streams))
|
Ok((children, indicator, streams))
|
||||||
}
|
}
|
||||||
fn validate_selection(
|
fn validate_selection(
|
||||||
@@ -501,10 +582,12 @@ fn validate_selection(
|
|||||||
}
|
}
|
||||||
Ok(selected)
|
Ok(selected)
|
||||||
}
|
}
|
||||||
async fn supervise(mut children: Vec<Child>, mut indicator: Child, cancel: Arc<Cancel>) {
|
async fn supervise(mut children: Vec<Child>, mut indicator: Option<Child>, cancel: Arc<Cancel>) {
|
||||||
loop {
|
loop {
|
||||||
if cancel.stopped.load(Ordering::SeqCst)
|
if cancel.stopped.load(Ordering::SeqCst)
|
||||||
|| !matches!(indicator.try_wait(), Ok(None))
|
|| indicator
|
||||||
|
.as_mut()
|
||||||
|
.is_some_and(|child| !matches!(child.try_wait(), Ok(None)))
|
||||||
|| children
|
|| children
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.any(|c| !matches!(c.try_wait(), Ok(None)))
|
.any(|c| !matches!(c.try_wait(), Ok(None)))
|
||||||
@@ -517,13 +600,19 @@ async fn supervise(mut children: Vec<Child>, mut indicator: Child, cancel: Arc<C
|
|||||||
for child in &mut children {
|
for child in &mut children {
|
||||||
let _ = child.kill().await;
|
let _ = child.kill().await;
|
||||||
}
|
}
|
||||||
|
if let Some(indicator) = &mut indicator {
|
||||||
let _ = indicator.kill().await;
|
let _ = indicator.kill().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let backend = Backend::default();
|
let backend = Backend::default();
|
||||||
let connection = zbus::connection::Builder::session()?
|
let connection = zbus::connection::Builder::session()?
|
||||||
.name("org.freedesktop.impl.portal.desktop.ferese")?
|
.name("org.freedesktop.impl.portal.desktop.ferese")?
|
||||||
.serve_at(PATH, backend.clone())?
|
.serve_at(PATH, backend.clone())?
|
||||||
|
.serve_at(
|
||||||
|
"/org/ferese/ScreenRecorder",
|
||||||
|
RecorderControl(backend.clone()),
|
||||||
|
)?
|
||||||
.build()
|
.build()
|
||||||
.await?;
|
.await?;
|
||||||
// Frontend death must revoke every stream, even if Session.Close never arrives.
|
// Frontend death must revoke every stream, even if Session.Close never arrives.
|
||||||
@@ -557,6 +646,22 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
|
fn bar_control_claims_are_bound_to_the_recorder_connection() {
|
||||||
|
assert!(owns_session(
|
||||||
|
":1.42",
|
||||||
|
"/org/freedesktop/portal/desktop/session/1_42/record"
|
||||||
|
));
|
||||||
|
assert!(!owns_session(
|
||||||
|
":1.4",
|
||||||
|
"/org/freedesktop/portal/desktop/session/1_42/record"
|
||||||
|
));
|
||||||
|
assert!(!owns_session(
|
||||||
|
":1.43",
|
||||||
|
"/org/freedesktop/portal/desktop/session/1_42/record"
|
||||||
|
));
|
||||||
|
assert!(!shell_recorder(std::process::id()));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
fn rejects_unimplemented_sources_and_cursor_modes() {
|
fn rejects_unimplemented_sources_and_cursor_modes() {
|
||||||
assert_eq!(source_options(&Options::new()).unwrap(), (false, false));
|
assert_eq!(source_options(&Options::new()).unwrap(), (false, false));
|
||||||
for bits in [0u32, 2, 3, 4] {
|
for bits in [0u32, 2, 3, 4] {
|
||||||
|
|||||||
+61
-5
@@ -27,6 +27,8 @@ struct Picker {
|
|||||||
core: Core,
|
core: Core,
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
selected: Vec<usize>,
|
selected: Vec<usize>,
|
||||||
|
background: cosmic::iced::Color,
|
||||||
|
foreground: cosmic::iced::Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -40,7 +42,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return Err("No shareable displays".into());
|
return Err("No shareable displays".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (theme, font) = appearance();
|
let (theme, font, background, foreground) = appearance();
|
||||||
let height = 156. + (prompt.sources.len().min(4) as f32 * 56.);
|
let height = 156. + (prompt.sources.len().min(4) as f32 * 56.);
|
||||||
cosmic::app::run::<Picker>(
|
cosmic::app::run::<Picker>(
|
||||||
Settings::default()
|
Settings::default()
|
||||||
@@ -49,7 +51,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.default_font(font)
|
.default_font(font)
|
||||||
.default_text_size(14.)
|
.default_text_size(14.)
|
||||||
.is_daemon(false),
|
.is_daemon(false),
|
||||||
prompt,
|
(prompt, background, foreground),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -57,7 +59,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
impl cosmic::Application for Picker {
|
impl cosmic::Application for Picker {
|
||||||
type Executor = cosmic::executor::Default;
|
type Executor = cosmic::executor::Default;
|
||||||
type Flags = Prompt;
|
type Flags = (Prompt, cosmic::iced::Color, cosmic::iced::Color);
|
||||||
type Message = Message;
|
type Message = Message;
|
||||||
const APP_ID: &'static str = "dev.ferese.ScreenShare";
|
const APP_ID: &'static str = "dev.ferese.ScreenShare";
|
||||||
|
|
||||||
@@ -69,14 +71,20 @@ impl cosmic::Application for Picker {
|
|||||||
&mut self.core
|
&mut self.core
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(mut core: Core, prompt: Prompt) -> (Self, Task<Message>) {
|
fn init(
|
||||||
|
mut core: Core,
|
||||||
|
(prompt, background, foreground): Self::Flags,
|
||||||
|
) -> (Self, Task<Message>) {
|
||||||
core.window.show_headerbar = false;
|
core.window.show_headerbar = false;
|
||||||
core.window.border_padding = Some(0);
|
core.window.border_padding = Some(0);
|
||||||
|
core.window.content_container = false;
|
||||||
// No preselected source: clicking Share must be a deliberate choice.
|
// No preselected source: clicking Share must be a deliberate choice.
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
core,
|
core,
|
||||||
prompt,
|
prompt,
|
||||||
selected: Vec::new(),
|
selected: Vec::new(),
|
||||||
|
background,
|
||||||
|
foreground,
|
||||||
};
|
};
|
||||||
let title = if app.prompt.indicator {
|
let title = if app.prompt.indicator {
|
||||||
"Ferese — screen sharing"
|
"Ferese — screen sharing"
|
||||||
@@ -122,6 +130,14 @@ impl cosmic::Application for Picker {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> Option<cosmic::iced::theme::Style> {
|
||||||
|
Some(cosmic::iced::theme::Style {
|
||||||
|
background_color: self.background,
|
||||||
|
text_color: self.foreground,
|
||||||
|
icon_color: self.foreground,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<'_, Message> {
|
fn view(&self) -> Element<'_, Message> {
|
||||||
let app = if self.prompt.app.is_empty() {
|
let app = if self.prompt.app.is_empty() {
|
||||||
"this application"
|
"this application"
|
||||||
@@ -234,7 +250,12 @@ fn glyph(source: &'static [u8], size: u16) -> icon::Icon {
|
|||||||
.size(size)
|
.size(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn appearance() -> (cosmic::Theme, cosmic::font::Font) {
|
fn appearance() -> (
|
||||||
|
cosmic::Theme,
|
||||||
|
cosmic::font::Font,
|
||||||
|
cosmic::iced::Color,
|
||||||
|
cosmic::iced::Color,
|
||||||
|
) {
|
||||||
let document = ferese_config::config_path()
|
let document = ferese_config::config_path()
|
||||||
.and_then(|p| std::fs::read_to_string(p).ok())
|
.and_then(|p| std::fs::read_to_string(p).ok())
|
||||||
.and_then(|s| ferese_config::Document::parse(&s).ok());
|
.and_then(|s| ferese_config::Document::parse(&s).ok());
|
||||||
@@ -296,6 +317,15 @@ fn appearance() -> (cosmic::Theme, cosmic::font::Font) {
|
|||||||
.corner_radii(corners)
|
.corner_radii(corners)
|
||||||
.build();
|
.build();
|
||||||
ferese_theme::apply(&mut native, foreground.into());
|
ferese_theme::apply(&mut native, foreground.into());
|
||||||
|
let mut material: cosmic::iced::Color = background.into();
|
||||||
|
material.a = material_opacity(
|
||||||
|
&string("theme.material.style", "solid"),
|
||||||
|
document
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.get("theme.surface.popover.opacity"))
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(0.96),
|
||||||
|
);
|
||||||
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||||
let family: &'static str =
|
let family: &'static str =
|
||||||
Box::leak(string("theme.typography.font_family", "Inter").into_boxed_str());
|
Box::leak(string("theme.typography.font_family", "Inter").into_boxed_str());
|
||||||
@@ -306,5 +336,31 @@ fn appearance() -> (cosmic::Theme, cosmic::font::Font) {
|
|||||||
family: cosmic::iced::font::Family::Name(family),
|
family: cosmic::iced::font::Family::Name(family),
|
||||||
..cosmic::font::default()
|
..cosmic::font::default()
|
||||||
},
|
},
|
||||||
|
material,
|
||||||
|
cosmic::iced::Color {
|
||||||
|
a: 1.,
|
||||||
|
..foreground.into()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only the surface tint is translucent; control fills and foregrounds stay opaque.
|
||||||
|
fn material_opacity(style: &str, opacity: f64) -> f32 {
|
||||||
|
if style == "translucent" && opacity.is_finite() {
|
||||||
|
opacity.clamp(0., 1.) as f32
|
||||||
|
} else {
|
||||||
|
1.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn material_preserves_solid_and_bounds_translucent_opacity() {
|
||||||
|
assert_eq!(super::material_opacity("solid", 0.4), 1.);
|
||||||
|
assert_eq!(super::material_opacity("translucent", 0.4), 0.4);
|
||||||
|
assert_eq!(super::material_opacity("translucent", 2.), 1.);
|
||||||
|
assert_eq!(super::material_opacity("translucent", -1.), 0.);
|
||||||
|
assert_eq!(super::material_opacity("translucent", f64::NAN), 1.);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,6 +136,21 @@ pub async fn run() -> Result<(), String> {
|
|||||||
.map_err(|e| e.to_string())?,
|
.map_err(|e| e.to_string())?,
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
// Older/foreign backends keep their normal sharing indicator. Only Ferese
|
||||||
|
// can verify this helper's identity and that its parent owns a bar stop button.
|
||||||
|
let _ = tokio::time::timeout(Duration::from_secs(3), async {
|
||||||
|
let control = Proxy::new(
|
||||||
|
&connection,
|
||||||
|
"org.freedesktop.impl.portal.desktop.ferese",
|
||||||
|
"/org/ferese/ScreenRecorder",
|
||||||
|
"org.ferese.ScreenRecorder",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
control
|
||||||
|
.call::<_, _, ()>("UseBarControls", &(handle.clone(),))
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
let session = Proxy::new(
|
let session = Proxy::new(
|
||||||
&connection,
|
&connection,
|
||||||
DESKTOP,
|
DESKTOP,
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ class PortalTest(unittest.TestCase):
|
|||||||
None, Gio.DBusCallFlags.NONE, 5000, None)
|
None, Gio.DBusCallFlags.NONE, 5000, None)
|
||||||
def create(token):
|
def create(token):
|
||||||
session = request("CreateSession", GLib.Variant("(a{sv})", ({"session_handle_token": GLib.Variant("s", token)},)))["session_handle"]
|
session = request("CreateSession", GLib.Variant("(a{sv})", ({"session_handle_token": GLib.Variant("s", token)},)))["session_handle"]
|
||||||
|
# An ordinary portal client cannot suppress its stop indicator.
|
||||||
|
with self.assertRaises(GLib.Error) as denied:
|
||||||
|
connection.call_sync("org.freedesktop.impl.portal.desktop.ferese", "/org/ferese/ScreenRecorder",
|
||||||
|
"org.ferese.ScreenRecorder", "UseBarControls", GLib.Variant("(o)", (session,)),
|
||||||
|
None, Gio.DBusCallFlags.NONE, 5000, None)
|
||||||
|
self.assertIn("AccessDenied", str(denied.exception))
|
||||||
request("SelectSources", GLib.Variant("(oa{sv})", (session, {"types": GLib.Variant("u", 1)})))
|
request("SelectSources", GLib.Variant("(oa{sv})", (session, {"types": GLib.Variant("u", 1)})))
|
||||||
return session
|
return session
|
||||||
def helpers():
|
def helpers():
|
||||||
|
|||||||
@@ -146,10 +146,10 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
|||||||
} else {
|
} else {
|
||||||
p.card
|
p.card
|
||||||
};
|
};
|
||||||
let on = if selected {
|
let (background, on) = if selected {
|
||||||
ferese_theme::foreground(background, p.text)
|
ferese_theme::accent_pair(background, p.text)
|
||||||
} else {
|
} else {
|
||||||
p.text
|
(background, p.text)
|
||||||
};
|
};
|
||||||
button::Style {
|
button::Style {
|
||||||
background: Some(Background::Color(background)),
|
background: Some(Background::Color(background)),
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ impl FereseShell {
|
|||||||
palette.surface_popover
|
palette.surface_popover
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let badge_text = ferese_theme::foreground(badge_fill, color(palette.text_primary));
|
let (badge_fill, badge_text) =
|
||||||
|
ferese_theme::accent_pair(badge_fill, color(palette.text_primary));
|
||||||
let badge = container(
|
let badge = container(
|
||||||
text(count.to_string())
|
text(count.to_string())
|
||||||
.size(10)
|
.size(10)
|
||||||
|
|||||||
Reference in new issue
Block a user