diff --git a/src/app/mod.rs b/src/app/mod.rs index e47e54f..35640ed 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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::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,