diff --git a/cosmic-settings/src/pages/networking/wifi.rs b/cosmic-settings/src/pages/networking/wifi.rs index 9047b1f..accec6a 100644 --- a/cosmic-settings/src/pages/networking/wifi.rs +++ b/cosmic-settings/src/pages/networking/wifi.rs @@ -83,6 +83,8 @@ pub enum Message { ViewMore(Option), /// Toggle WiFi access WiFiEnable(bool), + /// Update search query for filtering networks + SearchQuery(String), } impl From for crate::app::Message { @@ -137,6 +139,8 @@ pub struct Page { qr_code_data: Option, /// QR code context drawer state qr_drawer: Option, + /// Search query for filtering WiFi networks + search_query: String, } #[derive(Debug)] @@ -320,6 +324,7 @@ impl page::Page for Page { self.connecting.clear(); self.withheld_state = None; self.withheld_devices = None; + self.search_query.clear(); if let Some(cancel) = self.nm_task.take() { _ = cancel.send(()); @@ -664,6 +669,11 @@ impl Page { // TODO: Per-device wifi connection handling. self.active_device = Some(device); } + + Message::SearchQuery(query) => { + self.search_query = query; + } + Message::NetworkManagerConnect(conn) => { return cosmic::task::batch(vec![ self.connect(conn.clone()), @@ -857,6 +867,7 @@ fn devices_view() -> Section { forget_txt = fl!("wifi", "forget"); known_networks_txt = fl!("known-networks"); no_networks_txt = fl!("no-networks"); + no_search_results_txt = fl!("no-search-results"); settings_txt = fl!("settings"); share_txt = fl!("share"); visible_networks_txt = fl!("visible-networks"); @@ -897,47 +908,41 @@ fn devices_view() -> Section { view = view.push(no_networks_found); } else { + // Collect known SSIDs for deduplication + let known_ssids: BTreeSet<&str> = state + .known_access_points + .iter() + .map(|ap| ap.ssid.as_ref()) + .chain(state.active_conns.iter().filter_map(|active| { + if let ActiveConnectionInfo::WiFi { name, .. } = active { + Some(name.as_str()) + } else { + None + } + })) + .collect(); + + // Build Known Networks section (always unfiltered) + let mut known_networks = + widget::settings::section().title(§ion.descriptions[known_networks_txt]); let mut has_known = false; - let mut has_visible = false; - // Create separate sections for known and visible networks. - let (known_networks, visible_networks) = state.wireless_access_points.iter().fold( - ( - widget::settings::section() - .title(§ion.descriptions[known_networks_txt]), - widget::settings::section() - .title(§ion.descriptions[visible_networks_txt]), - ), - |(mut known_networks, mut visible_networks), network| { + // Add visible networks that are known + for network in &state.wireless_access_points { + if known_ssids.contains(network.ssid.as_ref()) { + has_known = true; let is_connected = is_connected(state, network); - - let is_known = state - .known_access_points - .iter() - .map(|known| known.ssid.as_ref()) - .chain(state.active_conns.iter().filter_map(|active| { - if let ActiveConnectionInfo::WiFi { name, .. } = active { - Some(name.as_str()) - } else { - None - } - })) - .any(|known| known == network.ssid.as_ref()); - + let is_known = known_ssids.contains(network.ssid.as_ref()); let needs_password = network.network_type != NetworkType::Open; - let (connect_txt, connect_msg) = if is_connected { + let (connect_label, connect_msg) = if is_connected { (§ion.descriptions[connected_txt], None) } else if page.connecting.contains(&network.ssid) { (§ion.descriptions[connecting_txt], None) } else { ( §ion.descriptions[connect_txt], - Some(if is_known || !needs_password { - Message::Connect(network.ssid.clone()) - } else { - Message::PasswordRequest(network.ssid.clone()) - }), + Some(Message::Connect(network.ssid.clone())), ) }; @@ -953,9 +958,9 @@ fn devices_view() -> Section { .spacing(spacing.space_xxs); let connect: Element<'_, Message> = if let Some(msg) = connect_msg { - widget::button::text(connect_txt).on_press(msg).into() + widget::button::text(connect_label).on_press(msg).into() } else { - widget::text::body(connect_txt) + widget::text::body(connect_label) .align_y(Alignment::Center) .into() }; @@ -1001,12 +1006,10 @@ fn devices_view() -> Section { .class(cosmic::theme::Container::Dropdown), ) .apply(|e| Some(Element::from(e))) - } else if is_known { + } else { view_more_button .on_press(Message::ViewMore(Some(network.ssid.clone()))) .apply(|e| Some(Element::from(e))) - } else { - None }; let controls = widget::row::with_capacity(2) @@ -1015,30 +1018,233 @@ fn devices_view() -> Section { .align_y(Alignment::Center) .spacing(spacing.space_xxs); - let widget = widget::settings::item_row(vec![ + let item = widget::settings::item_row(vec![ identifier.into(), widget::horizontal_space().into(), controls.into(), ]); - if is_known { - has_known = true; - known_networks = known_networks.add(widget); - } else { - has_visible = true; - visible_networks = visible_networks.add(widget); - } + known_networks = known_networks.add(item); + } + } - (known_networks, visible_networks) - }, - ); + // Also add known networks that are not currently visible + for network in &state.known_access_points { + let already_added = state + .wireless_access_points + .iter() + .any(|ap| ap.ssid == network.ssid); + if !already_added { + has_known = true; + let is_connected = is_connected(state, network); + let is_encrypted = network.network_type != NetworkType::Open; + let is_known = known_ssids.contains(network.ssid.as_ref()); + + let (connect_label, connect_msg) = if is_connected { + (§ion.descriptions[connected_txt], None) + } else if page.connecting.contains(&network.ssid) { + (§ion.descriptions[connecting_txt], None) + } else { + ( + §ion.descriptions[connect_txt], + Some(Message::Connect(network.ssid.clone())), + ) + }; + + let identifier = widget::row::with_capacity(3) + .push(widget::icon::from_name(wifi_icon(network.strength))) + .push_maybe( + is_encrypted + .then(|| widget::icon::from_name("connection-secure-symbolic")), + ) + .push( + widget::text::body(network.ssid.as_ref()).wrapping(Wrapping::Glyph), + ) + .spacing(spacing.space_xxs); + + let connect: Element<'_, Message> = if let Some(msg) = connect_msg { + widget::button::text(connect_label).on_press(msg).into() + } else { + widget::text::body(connect_label) + .align_y(Alignment::Center) + .into() + }; + + let view_more_button = + widget::button::icon(widget::icon::from_name("view-more-symbolic")); + + let view_more: Element<_> = if page + .view_more_popup + .as_deref() + .is_some_and(|id| id == network.ssid.as_ref()) + { + widget::popover(view_more_button.on_press(Message::ViewMore(None))) + .position(widget::popover::Position::Bottom) + .on_close(Message::ViewMore(None)) + .popup( + widget::column() + .push_maybe(is_connected.then(|| { + popup_button( + Message::Disconnect(network.ssid.clone()), + §ion.descriptions[disconnect_txt], + ) + })) + .push(popup_button( + Message::Settings(network.ssid.clone()), + §ion.descriptions[settings_txt], + )) + .push_maybe(is_known.then(|| { + popup_button( + Message::QRCodeRequest(network.ssid.clone()), + §ion.descriptions[share_txt], + ) + })) + .push_maybe(is_known.then(|| { + popup_button( + Message::ForgetRequest(network.ssid.clone()), + §ion.descriptions[forget_txt], + ) + })) + .width(Length::Fixed(200.0)) + .apply(widget::container) + .padding(cosmic::theme::spacing().space_xxs) + .class(cosmic::theme::Container::Dropdown), + ) + .into() + } else { + view_more_button + .on_press(Message::ViewMore(Some(network.ssid.clone()))) + .into() + }; + + let controls = widget::row::with_capacity(2) + .push(connect) + .push(view_more) + .align_y(Alignment::Center) + .spacing(spacing.space_xxs); + + let item = widget::settings::item_row(vec![ + identifier.into(), + widget::horizontal_space().into(), + controls.into(), + ]); + + known_networks = known_networks.add(item); + } + } if has_known { view = view.push(known_networks); } - if has_visible { - view = view.push(visible_networks); + // Build Visible Networks section (searchable when 15+ networks, filtered when user types) + let show_search = state.wireless_access_points.len() >= 15; + let search_query_lower = page.search_query.trim().to_lowercase(); + + // Filter visible networks (exclude known networks, apply search filter) + let filtered_visible: Vec<_> = state + .wireless_access_points + .iter() + .filter(|network| !known_ssids.contains(network.ssid.as_ref())) + .filter(|network| { + if show_search && !search_query_lower.is_empty() { + network + .ssid + .as_ref() + .to_lowercase() + .contains(&search_query_lower) + } else { + true + } + }) + .collect(); + + // Check if we have any visible (non-known) networks at all + let has_any_visible = state + .wireless_access_points + .iter() + .any(|network| !known_ssids.contains(network.ssid.as_ref())); + + // Only show visible networks section if there are non-known networks + if has_any_visible { + // Build visible networks section with optional search + let mut visible_section = widget::column::with_capacity(3); + + // Section title + visible_section = visible_section.push(widget::text::title4( + §ion.descriptions[visible_networks_txt], + )); + + // Search input (only shown when 15+ networks) + if show_search { + let search_input = + widget::search_input(fl!("type-to-search"), &page.search_query) + .on_input(Message::SearchQuery) + .on_clear(Message::SearchQuery(String::new())); + visible_section = visible_section.push(search_input); + } + + // Network list or "no results" message + if filtered_visible.is_empty() && show_search && !search_query_lower.is_empty() + { + // Show "no search results" message only when search is active and returns no results + visible_section = visible_section.push( + widget::container(widget::text::body( + §ion.descriptions[no_search_results_txt], + )) + .center_x(Length::Fill), + ); + } else if !filtered_visible.is_empty() { + let mut visible_networks_list = widget::list_column(); + for network in filtered_visible { + let is_encrypted = network.network_type != NetworkType::Open; + + let (connect_label, connect_msg) = + if page.connecting.contains(&network.ssid) { + (§ion.descriptions[connecting_txt], None) + } else { + ( + §ion.descriptions[connect_txt], + Some(if is_encrypted { + Message::PasswordRequest(network.ssid.clone()) + } else { + Message::Connect(network.ssid.clone()) + }), + ) + }; + + let identifier = + widget::row::with_capacity(3) + .push(widget::icon::from_name(wifi_icon(network.strength))) + .push_maybe(is_encrypted.then(|| { + widget::icon::from_name("connection-secure-symbolic") + })) + .push( + widget::text::body(network.ssid.as_ref()) + .wrapping(Wrapping::Glyph), + ) + .spacing(spacing.space_xxs); + + let connect: Element<'_, Message> = if let Some(msg) = connect_msg { + widget::button::text(connect_label).on_press(msg).into() + } else { + widget::text::body(connect_label) + .align_y(Alignment::Center) + .into() + }; + + let item = widget::settings::item_row(vec![ + identifier.into(), + widget::horizontal_space().into(), + connect, + ]); + + visible_networks_list = visible_networks_list.add(item); + } + visible_section = visible_section.push(visible_networks_list); + } + + view = view.push(visible_section.spacing(spacing.space_xs)); } }; diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 51bcf00..735d892 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -715,6 +715,7 @@ keyboard-numlock-boot = Numlock added = Added type-to-search = Type to search... +no-search-results = No networks match your search. show-extended-input-sources = Show extended input sources ## Input: Keyboard: Shortcuts