use crate::state::AppState;
const fn is_point_in_rect(mx: u16, my: u16, rect: Option<(u16, u16, u16, u16)>) -> bool {
if let Some((x, y, w, h)) = rect {
mx >= x && mx < x + w && my >= y && my < y + h
} else {
false
}
}
fn try_toggle_simple_filter<F>(
mx: u16,
my: u16,
rect: Option<(u16, u16, u16, u16)>,
toggle_fn: F,
app: &mut AppState,
) -> bool
where
F: FnOnce(&mut AppState),
{
if is_point_in_rect(mx, my, rect) {
toggle_fn(app);
crate::logic::apply_filters_and_sort_preserve_selection(app);
true
} else {
false
}
}
fn toggle_all_artix_filters(app: &mut AppState) {
let all_on = app.results_filter_show_artix_omniverse
&& app.results_filter_show_artix_universe
&& app.results_filter_show_artix_lib32
&& app.results_filter_show_artix_galaxy
&& app.results_filter_show_artix_world
&& app.results_filter_show_artix_system;
let new_state = !all_on;
app.results_filter_show_artix_omniverse = new_state;
app.results_filter_show_artix_universe = new_state;
app.results_filter_show_artix_lib32 = new_state;
app.results_filter_show_artix_galaxy = new_state;
app.results_filter_show_artix_world = new_state;
app.results_filter_show_artix_system = new_state;
app.results_filter_show_artix = new_state;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
const fn has_hidden_artix_filters(app: &AppState) -> bool {
app.results_filter_artix_omniverse_rect.is_none()
&& app.results_filter_artix_universe_rect.is_none()
&& app.results_filter_artix_lib32_rect.is_none()
&& app.results_filter_artix_galaxy_rect.is_none()
&& app.results_filter_artix_world_rect.is_none()
&& app.results_filter_artix_system_rect.is_none()
}
fn handle_artix_main_filter_click(mx: u16, my: u16, app: &mut AppState) -> bool {
if !is_point_in_rect(mx, my, app.results_filter_artix_rect) {
return false;
}
app.custom_repos_filter_menu_open = false;
if has_hidden_artix_filters(app) {
app.artix_filter_menu_open = !app.artix_filter_menu_open;
} else {
toggle_all_artix_filters(app);
}
true
}
fn handle_custom_repos_chip_click(mx: u16, my: u16, app: &mut AppState) -> bool {
if app.results_filter_dynamic.is_empty() {
return false;
}
if !is_point_in_rect(mx, my, app.results_filter_custom_repos_rect) {
return false;
}
app.artix_filter_menu_open = false;
app.custom_repos_filter_menu_open = !app.custom_repos_filter_menu_open;
true
}
fn handle_custom_repos_dropdown_click(mx: u16, my: u16, app: &mut AppState) -> bool {
if !app.custom_repos_filter_menu_open {
return false;
}
let Some((x, y, w, h)) = app.custom_repos_filter_menu_rect else {
return false;
};
if !is_point_in_rect(mx, my, Some((x, y, w, h))) {
return false;
}
let row = my.saturating_sub(y) as usize;
let mut keys: Vec<String> = app.results_filter_dynamic.keys().cloned().collect();
keys.sort();
if row == 0 {
let all_on = app.results_filter_dynamic.values().all(|v| *v);
crate::logic::repos::persist_dynamic_filters_set_all(app, !all_on);
return true;
}
let idx = row.saturating_sub(1);
let Some(id) = keys.get(idx) else {
return false;
};
let cur = app.results_filter_dynamic.get(id).copied().unwrap_or(true);
crate::logic::repos::persist_dynamic_filter_toggle_and_refresh(app, id, !cur);
true
}
#[allow(clippy::missing_const_for_fn)]
fn update_main_artix_filter_state(app: &mut AppState) {
app.results_filter_show_artix = app.results_filter_show_artix_omniverse
|| app.results_filter_show_artix_universe
|| app.results_filter_show_artix_lib32
|| app.results_filter_show_artix_galaxy
|| app.results_filter_show_artix_world
|| app.results_filter_show_artix_system;
}
fn handle_artix_dropdown_click(mx: u16, my: u16, app: &mut AppState) -> bool {
if !app.artix_filter_menu_open {
return false;
}
let Some((x, y, w, h)) = app.artix_filter_menu_rect else {
return false;
};
if !is_point_in_rect(mx, my, Some((x, y, w, h))) {
return false;
}
let row = my.saturating_sub(y) as usize;
match row {
0 => toggle_all_artix_filters(app),
1 => {
app.results_filter_show_artix_omniverse = !app.results_filter_show_artix_omniverse;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
2 => {
app.results_filter_show_artix_universe = !app.results_filter_show_artix_universe;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
3 => {
app.results_filter_show_artix_lib32 = !app.results_filter_show_artix_lib32;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
4 => {
app.results_filter_show_artix_galaxy = !app.results_filter_show_artix_galaxy;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
5 => {
app.results_filter_show_artix_world = !app.results_filter_show_artix_world;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
6 => {
app.results_filter_show_artix_system = !app.results_filter_show_artix_system;
crate::logic::apply_filters_and_sort_preserve_selection(app);
}
_ => return false,
}
update_main_artix_filter_state(app);
true
}
pub(super) fn handle_filters_mouse(mx: u16, my: u16, app: &mut AppState) -> Option<bool> {
if matches!(app.app_mode, crate::state::types::AppMode::News) {
let mut handled = false;
if is_point_in_rect(mx, my, app.news_filter_arch_rect) {
app.news_filter_show_arch_news = !app.news_filter_show_arch_news;
handled = true;
} else if is_point_in_rect(mx, my, app.news_filter_advisory_rect) {
match (
app.news_filter_show_advisories,
app.news_filter_installed_only,
) {
(true, false) => {
app.news_filter_show_advisories = true;
app.news_filter_installed_only = true;
}
(true, true) => {
app.news_filter_show_advisories = false;
app.news_filter_installed_only = false;
}
(false, _) => {
app.news_filter_show_advisories = true;
app.news_filter_installed_only = false;
}
}
handled = true;
} else if is_point_in_rect(mx, my, app.news_filter_updates_rect) {
app.news_filter_show_pkg_updates = !app.news_filter_show_pkg_updates;
handled = true;
} else if is_point_in_rect(mx, my, app.news_filter_aur_updates_rect) {
app.news_filter_show_aur_updates = !app.news_filter_show_aur_updates;
handled = true;
} else if is_point_in_rect(mx, my, app.news_filter_aur_comments_rect) {
app.news_filter_show_aur_comments = !app.news_filter_show_aur_comments;
handled = true;
} else if is_point_in_rect(mx, my, app.news_filter_read_rect) {
app.news_filter_read_status = match app.news_filter_read_status {
crate::state::types::NewsReadFilter::All => {
crate::state::types::NewsReadFilter::Unread
}
crate::state::types::NewsReadFilter::Unread => {
crate::state::types::NewsReadFilter::Read
}
crate::state::types::NewsReadFilter::Read => {
crate::state::types::NewsReadFilter::All
}
};
handled = true;
}
if handled {
crate::theme::save_news_filter_show_arch_news(app.news_filter_show_arch_news);
crate::theme::save_news_filter_show_advisories(app.news_filter_show_advisories);
crate::theme::save_news_filter_show_pkg_updates(app.news_filter_show_pkg_updates);
crate::theme::save_news_filter_show_aur_updates(app.news_filter_show_aur_updates);
crate::theme::save_news_filter_show_aur_comments(app.news_filter_show_aur_comments);
crate::theme::save_news_filter_installed_only(app.news_filter_installed_only);
app.refresh_news_results();
return Some(false);
}
return None;
}
if handle_custom_repos_dropdown_click(mx, my, app) {
return Some(false);
}
if handle_artix_dropdown_click(mx, my, app) {
return Some(false);
}
if handle_custom_repos_chip_click(mx, my, app) {
return Some(false);
}
if handle_artix_main_filter_click(mx, my, app) {
return Some(false);
}
if handle_simple_filter_toggles(mx, my, app) {
return Some(false);
}
None
}
fn handle_simple_filter_toggles(mx: u16, my: u16, app: &mut AppState) -> bool {
try_toggle_simple_filter(
mx,
my,
app.results_filter_aur_rect,
|a| a.results_filter_show_aur = !a.results_filter_show_aur,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_core_rect,
|a| a.results_filter_show_core = !a.results_filter_show_core,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_extra_rect,
|a| a.results_filter_show_extra = !a.results_filter_show_extra,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_multilib_rect,
|a| a.results_filter_show_multilib = !a.results_filter_show_multilib,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_eos_rect,
|a| a.results_filter_show_eos = !a.results_filter_show_eos,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_cachyos_rect,
|a| a.results_filter_show_cachyos = !a.results_filter_show_cachyos,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_artix_omniverse_rect,
|a| a.results_filter_show_artix_omniverse = !a.results_filter_show_artix_omniverse,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_artix_universe_rect,
|a| a.results_filter_show_artix_universe = !a.results_filter_show_artix_universe,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_artix_lib32_rect,
|a| a.results_filter_show_artix_lib32 = !a.results_filter_show_artix_lib32,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_artix_galaxy_rect,
|a| a.results_filter_show_artix_galaxy = !a.results_filter_show_artix_galaxy,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_artix_world_rect,
|a| a.results_filter_show_artix_world = !a.results_filter_show_artix_world,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_artix_system_rect,
|a| a.results_filter_show_artix_system = !a.results_filter_show_artix_system,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_blackarch_rect,
|a| a.results_filter_show_blackarch = !a.results_filter_show_blackarch,
app,
) || try_toggle_simple_filter(
mx,
my,
app.results_filter_manjaro_rect,
|a| a.results_filter_show_manjaro = !a.results_filter_show_manjaro,
app,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::types::{NewsFeedSource, NewsReadFilter, NewsSortMode};
fn sample_news_items() -> Vec<crate::state::types::NewsFeedItem> {
vec![
crate::state::types::NewsFeedItem {
id: "arch-news-1".to_string(),
date: "2025-01-01".to_string(),
title: "Arch item".to_string(),
summary: None,
url: None,
source: NewsFeedSource::ArchNews,
severity: None,
packages: Vec::new(),
},
crate::state::types::NewsFeedItem {
id: "adv-1".to_string(),
date: "2025-01-02".to_string(),
title: "Advisory item".to_string(),
summary: None,
url: None,
source: NewsFeedSource::SecurityAdvisory,
severity: None,
packages: vec!["example".to_string()],
},
]
}
#[test]
fn news_filter_click_toggles_flags_and_refreshes() {
let mut app = crate::state::AppState {
app_mode: crate::state::types::AppMode::News,
news_items: sample_news_items(),
news_filter_show_arch_news: true,
news_filter_show_advisories: true,
news_filter_installed_only: false,
news_sort_mode: NewsSortMode::DateDesc,
news_filter_arch_rect: Some((0, 0, 5, 1)),
..crate::state::AppState::default()
};
app.refresh_news_results();
let handled = handle_filters_mouse(0, 0, &mut app);
assert_eq!(handled, Some(false));
assert!(!app.news_filter_show_arch_news);
assert!(app.news_results.len() <= app.news_items.len());
assert!(
app.news_results
.iter()
.all(|i| matches!(i.source, NewsFeedSource::SecurityAdvisory))
);
}
#[test]
fn news_read_filter_click_cycles_status() {
let mut app = crate::state::AppState {
app_mode: crate::state::types::AppMode::News,
news_items: sample_news_items(),
news_filter_read_status: NewsReadFilter::All,
news_filter_read_rect: Some((0, 0, 6, 1)),
..crate::state::AppState::default()
};
app.refresh_news_results();
let handled1 = handle_filters_mouse(0, 0, &mut app);
assert_eq!(handled1, Some(false));
assert_eq!(app.news_filter_read_status, NewsReadFilter::Unread);
let handled2 = handle_filters_mouse(0, 0, &mut app);
assert_eq!(handled2, Some(false));
assert_eq!(app.news_filter_read_status, NewsReadFilter::Read);
let handled3 = handle_filters_mouse(0, 0, &mut app);
assert_eq!(handled3, Some(false));
assert_eq!(app.news_filter_read_status, NewsReadFilter::All);
}
}