fix(ui): explainer popup for screen share instead of a dead "Needs pixelpass" button (A11)

Screen sharing relies on the optional pixelpass companion CLI. Previously, when
it wasn't installed the Share Screen button was disabled and relabelled "Needs
pixelpass", which read as peerspeak advertising a broken in-app feature.

Now the Share Screen (and a peer's Watch) buttons stay enabled; if pixelpass
isn't on PATH, clicking opens a short explainer popup describing pixelpass as an
optional P2P-video companion and how to enable it (install pixelpass + mpv).
When pixelpass is present, behaviour is unchanged (toggles the share / opens the
viewer). Popup reuses the existing centered-modal + backdrop pattern.

Build + clippy clean. Manual check: with pixelpass off PATH, create a room and
click Share Screen — the explainer appears; backdrop / ✕ / "Got it" dismiss it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 15:41:04 -04:00
co-authored by Claude Opus 4.8
parent 3d2124ed98
commit 7af8edd5ae
+126 -15
View File
@@ -150,6 +150,9 @@ pub enum AppMessage {
/// Open / close the room-layout picker popup.
OpenLayoutPicker,
CloseLayoutPicker,
/// Open / close the "screen sharing needs pixelpass" explainer popup (A11).
OpenPixelpassHelp,
ClosePixelpassHelp,
/// Choose a room layout (applied live + persisted, closes the popup).
SelectRoomLayout(RoomLayout),
/// Choose a UI theme (applied live + persisted).
@@ -212,6 +215,8 @@ pub struct AppState {
window_size: Size,
/// Whether the room-layout picker popup is open (launch + in-call screens).
layout_picker_open: bool,
/// Whether the pixelpass screen-share explainer popup is open (A11).
pixelpass_help_open: bool,
/// Whether the Chat drawer is open (drawer layout only).
drawer_chat_open: bool,
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
@@ -309,6 +314,7 @@ impl Default for AppState {
chat_input: String::new(),
window_size: Size::new(ww, wh),
layout_picker_open: false,
pixelpass_help_open: false,
drawer_chat_open: false,
mic_level: 0.0,
mic_test_active: false,
@@ -748,6 +754,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
AppMessage::CloseLayoutPicker => {
state.layout_picker_open = false;
}
AppMessage::OpenPixelpassHelp => {
state.pixelpass_help_open = true;
}
AppMessage::ClosePixelpassHelp => {
state.pixelpass_help_open = false;
}
AppMessage::SelectRoomLayout(layout) => {
state.config.room_layout = layout;
state.config.save();
@@ -1656,9 +1668,13 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
)
.style(b_style(color_blue, color_lavender, color_crust, 6.0))
.padding(6);
if state.pixelpass_available {
watch_btn = watch_btn.on_press(AppMessage::WatchShare(ticket));
}
// Always actionable: opens the pixelpass explainer if it's missing,
// otherwise launches the viewer (A11 — consistent with Share Screen).
watch_btn = watch_btn.on_press(if state.pixelpass_available {
AppMessage::WatchShare(ticket)
} else {
AppMessage::OpenPixelpassHelp
});
row![
row![
icon(IconKind::Live, 13.0, color_red),
@@ -1816,24 +1832,26 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
},
vertical_space(20.0),
{
// Screen share. Disabled (no on_press) when pixelpass is absent,
// with the label saying so — a normal, handled state.
// Screen share. Always enabled and labelled "Share Screen": if the
// optional pixelpass companion isn't installed, clicking opens a
// short how-to-install explainer instead of being a dead disabled
// button (A11) — so it never reads as a broken in-app feature.
let (share_kind, share_label, share_bg, share_hover, share_fg) =
if !state.pixelpass_available {
(IconKind::Monitor, "Needs pixelpass", color_surface, color_surface, color_subtext)
} else if state.self_sharing {
if state.self_sharing {
(IconKind::Stop, "Stop Sharing", color_red, color_maroon, color_crust)
} else {
(IconKind::Monitor, "Share Screen", color_surface, color_blue, color_text)
};
let mut share_btn = button(btn_content(share_kind, share_label, share_fg))
let share_press = if state.pixelpass_available {
AppMessage::ToggleScreenShare
} else {
AppMessage::OpenPixelpassHelp
};
button(btn_content(share_kind, share_label, share_fg))
.on_press(share_press)
.style(b_style(share_bg, share_hover, share_fg, 8.0))
.padding(14)
.width(iced::Length::Fill);
if state.pixelpass_available {
share_btn = share_btn.on_press(AppMessage::ToggleScreenShare);
}
share_btn
.width(iced::Length::Fill)
}
];
@@ -2020,7 +2038,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.height(iced::Length::Fill)
.style(c_style(color_crust, Color::TRANSPARENT, 0.0));
with_layout_picker(room.into(), state)
with_pixelpass_help(with_layout_picker(room.into(), state), state)
}
}
@@ -2390,6 +2408,99 @@ fn with_layout_picker<'a>(
.into()
}
/// Overlays the "screen sharing needs pixelpass" explainer popup over `base`
/// when open (A11). Triggered by the Share Screen / Watch controls when the
/// optional `pixelpass` companion isn't installed, so those controls open a
/// short how-to instead of being dead/disabled. Returns the base unchanged when
/// the popup is closed.
fn with_pixelpass_help<'a>(
base: Element<'a, AppMessage>,
state: &'a AppState,
) -> Element<'a, AppMessage> {
if !state.pixelpass_help_open {
return base;
}
let pal = state.config.theme.palette();
let crust = pal.crust;
let mantle = pal.mantle;
let surface = pal.surface;
let text_c = pal.text;
let subtext = pal.subtext;
let blue = pal.blue;
let backdrop = mouse_area(
container(horizontal_space())
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.style(move |_t: &Theme| container::Style {
background: Some(Background::Color(Color { a: 0.55, ..crust })),
..Default::default()
}),
)
.on_press(AppMessage::ClosePixelpassHelp);
let dialog = container(
column![
row![
text("Screen sharing").size(16).color(blue),
horizontal_space(),
button(text("").size(16).color(subtext))
.on_press(AppMessage::ClosePixelpassHelp)
.style(|_t: &Theme, _s: button::Status| button::Style {
background: None,
..Default::default()
})
.padding(2),
]
.align_y(iced::alignment::Vertical::Center),
text(
"Screen sharing uses pixelpass, a small companion tool that \
streams video peer-to-peer alongside your call. It's optional \
and ships separately, so peerspeak works fully without it."
)
.size(13)
.color(text_c),
text("To enable sharing and watching:").size(13).color(text_c),
text("• Install pixelpass and make sure the `pixelpass` command is on your PATH.").size(12).color(subtext),
text("• Install `mpv` too — it's what opens a peer's shared screen.").size(12).color(subtext),
text("Once both are present, restart peerspeak and the Share Screen button will start a share.").size(12).color(subtext),
row![
horizontal_space(),
button(text("Got it").size(13))
.on_press(AppMessage::ClosePixelpassHelp)
.style(move |_t: &Theme, status: button::Status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered => blue,
_ => surface,
})),
text_color: text_c,
border: Border { color: Color::TRANSPARENT, width: 0.0, radius: 6.0.into() },
..Default::default()
})
.padding(8),
],
]
.spacing(14),
)
.style(move |_t: &Theme| container::Style {
text_color: Some(text_c),
background: Some(Background::Color(mantle)),
border: Border { color: surface, width: 1.0, radius: 12.0.into() },
..Default::default()
})
.padding(20)
.width(iced::Length::Fixed(460.0));
stack![
base,
backdrop,
container(dialog)
.center_x(iced::Length::Fill)
.center_y(iced::Length::Fill),
]
.into()
}
/// A small two-pane glyph for the square layout-picker button in the top bar.
struct LayoutIcon {
fg: Color,