Skip to content

Commit 126cbac

Browse files
committed
Implement pagination for Containers, Images, and Volumes tabs in Cosmic Containers applet. Introduce tab page management with navigation buttons, and refactor UI components to support paginated lists. Update related functions and tests to ensure correct behavior and enhance user experience.
1 parent 0712d14 commit 126cbac

7 files changed

Lines changed: 268 additions & 31 deletions

File tree

‎src/app/mod.rs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@ pub enum PendingRemove {
2626
Volume(String),
2727
}
2828

29+
/// Per-tab zero-based page index for paginated list tabs.
30+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31+
pub(super) struct TabPages {
32+
pub(super) containers: usize,
33+
pub(super) images: usize,
34+
pub(super) volumes: usize,
35+
}
36+
37+
impl TabPages {
38+
pub(super) fn page_mut(&mut self, tab: PopupTab) -> Option<&mut usize> {
39+
match tab {
40+
PopupTab::Containers => Some(&mut self.containers),
41+
PopupTab::Images => Some(&mut self.images),
42+
PopupTab::Volumes => Some(&mut self.volumes),
43+
PopupTab::Settings => None,
44+
}
45+
}
46+
}
47+
2948
/// COSMIC panel applet state.
3049
#[derive(Default)]
3150
pub struct App {
@@ -37,6 +56,7 @@ pub struct App {
3756
pub(super) image_list: Vec<ImageSummary>,
3857
pub(super) volume_list: Vec<VolumeSummary>,
3958
pub(super) collapsed_groups: HashSet<String>,
59+
pub(super) tab_pages: TabPages,
4060
pub(super) pending_remove: Option<PendingRemove>,
4161
pub(super) loading: bool,
4262
pub(super) error: Option<String>,
@@ -51,6 +71,8 @@ pub enum Message {
5171
Poll,
5272
Refresh,
5373
TabSelected(PopupTab),
74+
TabPagePrev,
75+
TabPageNext,
5476
ToggleGroupCollapse(String),
5577
RequestRemove(PendingRemove),
5678
ConfirmRemove,
@@ -88,6 +110,29 @@ impl App {
88110
self.groups = group_containers(self.containers.clone());
89111
}
90112

113+
pub(super) fn container_row_count(&self) -> usize {
114+
self.groups.iter().fold(0, |count, group| {
115+
let collapsed = self.collapsed_groups.contains(&group.project);
116+
count + 1 + if collapsed { 0 } else { group.containers.len() }
117+
})
118+
}
119+
120+
pub(super) fn clamp_tab_page(&mut self, tab: PopupTab) {
121+
use linux_pop_extension::{LIST_PAGE_SIZE, clamp_page, total_pages};
122+
123+
let item_count = match tab {
124+
PopupTab::Containers => self.container_row_count(),
125+
PopupTab::Images => self.image_list.len(),
126+
PopupTab::Volumes => self.volume_list.len(),
127+
PopupTab::Settings => return,
128+
};
129+
let total = total_pages(item_count, LIST_PAGE_SIZE);
130+
let Some(page) = self.tab_pages.page_mut(tab) else {
131+
return;
132+
};
133+
*page = clamp_page(*page, total);
134+
}
135+
91136
pub(super) fn apply_containers(
92137
&mut self,
93138
result: Result<Vec<ContainerSummary>, String>,
@@ -100,6 +145,7 @@ impl App {
100145
Ok(containers) => {
101146
self.containers = containers;
102147
self.rebuild_groups();
148+
self.clamp_tab_page(PopupTab::Containers);
103149
if self.active_tab == PopupTab::Containers {
104150
self.error = None;
105151
}
@@ -119,6 +165,7 @@ impl App {
119165
match result {
120166
Ok(image_list) => {
121167
self.image_list = image_list;
168+
self.clamp_tab_page(PopupTab::Images);
122169
if self.active_tab == PopupTab::Images {
123170
self.error = None;
124171
}
@@ -142,6 +189,7 @@ impl App {
142189
match result {
143190
Ok(volume_list) => {
144191
self.volume_list = volume_list;
192+
self.clamp_tab_page(PopupTab::Volumes);
145193
if self.active_tab == PopupTab::Volumes {
146194
self.error = None;
147195
}

‎src/app/popup/containers.rs‎

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,75 @@ use cosmic::iced::alignment::Alignment;
44
use cosmic::widget;
55
use linux_pop_extension::action_icons;
66
use linux_pop_extension::icons;
7-
use linux_pop_extension::{ContainerGroup, ContainerSummary};
7+
use linux_pop_extension::{
8+
ContainerGroup, ContainerSummary, LIST_PAGE_SIZE, clamp_page, page_range, total_pages,
9+
};
810

911
use super::layout::{FILL_PORTION_PROMPT, PAD_GROUP_HEADER, SPACING_NONE, SPACING_SM, SPACING_XXS};
1012
use super::widgets::{
11-
action_icon_button, action_svg_button, empty_state, item_labels, list_row, remove_confirm_row,
12-
scrollable_list, section_divider,
13+
action_icon_button, action_svg_button, empty_state, item_labels, list_row, paginated_list,
14+
remove_confirm_row, section_divider,
1315
};
1416
use super::{App, Message};
1517
use crate::app::PendingRemove;
1618

19+
enum ContainerListRow<'a> {
20+
GroupHeader {
21+
group: &'a ContainerGroup,
22+
collapsed: bool,
23+
},
24+
Container {
25+
container: &'a ContainerSummary,
26+
},
27+
}
28+
1729
pub(super) fn containers_tab<'a>(state: &'a App) -> Element<'a, Message> {
1830
if state.groups.is_empty() && !state.loading && state.error.is_none() {
1931
return empty_state("No containers");
2032
}
2133

34+
let rows = build_container_rows(state);
35+
let total = total_pages(rows.len(), LIST_PAGE_SIZE);
36+
let page = clamp_page(state.tab_pages.containers, total);
37+
let range = page_range(page, LIST_PAGE_SIZE, rows.len());
38+
2239
let mut list = widget::column![].spacing(SPACING_NONE);
23-
for (index, group) in state.groups.iter().enumerate() {
24-
if index > 0 {
40+
for (index, row) in rows[range].iter().enumerate() {
41+
if index > 0 && matches!(row, ContainerListRow::GroupHeader { .. }) {
2542
list = list.push(section_divider());
2643
}
27-
list = list.push(group_section(state, group));
44+
match row {
45+
ContainerListRow::GroupHeader { group, collapsed } => {
46+
list = list.push(group_header_row(
47+
group,
48+
*collapsed,
49+
group_toolbar(group, *collapsed),
50+
));
51+
}
52+
ContainerListRow::Container { container } => {
53+
list = list.push(widget::container(container_row(
54+
container,
55+
&state.pending_remove,
56+
)));
57+
}
58+
}
59+
}
60+
61+
paginated_list(list.into(), page, total)
62+
}
63+
64+
fn build_container_rows<'a>(state: &'a App) -> Vec<ContainerListRow<'a>> {
65+
let mut rows = Vec::new();
66+
for group in &state.groups {
67+
let collapsed = state.collapsed_groups.contains(&group.project);
68+
rows.push(ContainerListRow::GroupHeader { group, collapsed });
69+
if !collapsed {
70+
for container in &group.containers {
71+
rows.push(ContainerListRow::Container { container });
72+
}
73+
}
2874
}
29-
scrollable_list(list.into())
75+
rows
3076
}
3177

3278
fn container_state_line(container: &ContainerSummary) -> String {
@@ -112,7 +158,7 @@ fn group_toolbar<'a>(group: &'a ContainerGroup, collapsed: bool) -> Element<'a,
112158
actions.into()
113159
}
114160

115-
fn group_header<'a>(
161+
fn group_header_row<'a>(
116162
group: &'a ContainerGroup,
117163
collapsed: bool,
118164
toolbar: Element<'a, Message>,
@@ -145,19 +191,3 @@ fn group_header<'a>(
145191
.width(Length::Fill)
146192
.into()
147193
}
148-
149-
fn group_section<'a>(state: &'a App, group: &'a ContainerGroup) -> Element<'a, Message> {
150-
let collapsed = state.collapsed_groups.contains(&group.project);
151-
let header = group_header(group, collapsed, group_toolbar(group, collapsed));
152-
153-
let mut section = widget::column![header].spacing(SPACING_NONE);
154-
if !collapsed {
155-
for container in &group.containers {
156-
section = section.push(widget::container(container_row(
157-
container,
158-
&state.pending_remove,
159-
)));
160-
}
161-
}
162-
section.into()
163-
}

‎src/app/popup/images_tab.rs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ use cosmic::Element;
22
use cosmic::widget;
33
use linux_pop_extension::ImageSummary;
44
use linux_pop_extension::action_icons;
5+
use linux_pop_extension::{LIST_PAGE_SIZE, clamp_page, page_range, total_pages};
56

67
use super::layout::SPACING_NONE;
78
use super::layout::SPACING_XXS;
89
use super::widgets::{
9-
action_svg_button, empty_state, item_labels, list_row, remove_confirm_row, scrollable_list,
10+
action_svg_button, empty_state, item_labels, list_row, paginated_list, remove_confirm_row,
1011
};
1112
use super::{App, Message};
1213
use crate::app::PendingRemove;
@@ -16,11 +17,15 @@ pub(super) fn images_tab<'a>(state: &'a App) -> Element<'a, Message> {
1617
return empty_state("No images");
1718
}
1819

20+
let total = total_pages(state.image_list.len(), LIST_PAGE_SIZE);
21+
let page = clamp_page(state.tab_pages.images, total);
22+
let range = page_range(page, LIST_PAGE_SIZE, state.image_list.len());
23+
1924
let mut list = widget::column![].spacing(SPACING_NONE);
20-
for image in &state.image_list {
25+
for image in &state.image_list[range] {
2126
list = list.push(image_row(image, &state.pending_remove));
2227
}
23-
scrollable_list(list.into())
28+
paginated_list(list.into(), page, total)
2429
}
2530

2631
fn image_row<'a>(

‎src/app/popup/volumes_tab.rs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ use cosmic::Element;
22
use cosmic::widget;
33
use linux_pop_extension::VolumeSummary;
44
use linux_pop_extension::action_icons;
5+
use linux_pop_extension::{LIST_PAGE_SIZE, clamp_page, page_range, total_pages};
56

67
use super::layout::SPACING_NONE;
78
use super::layout::SPACING_XXS;
89
use super::widgets::{
9-
action_svg_button, empty_state, item_labels, list_row, remove_confirm_row, scrollable_list,
10+
action_svg_button, empty_state, item_labels, list_row, paginated_list, remove_confirm_row,
1011
};
1112
use super::{App, Message};
1213
use crate::app::PendingRemove;
@@ -16,11 +17,15 @@ pub(super) fn volumes_tab<'a>(state: &'a App) -> Element<'a, Message> {
1617
return empty_state("No volumes");
1718
}
1819

20+
let total = total_pages(state.volume_list.len(), LIST_PAGE_SIZE);
21+
let page = clamp_page(state.tab_pages.volumes, total);
22+
let range = page_range(page, LIST_PAGE_SIZE, state.volume_list.len());
23+
1924
let mut list = widget::column![].spacing(SPACING_NONE);
20-
for volume in &state.volume_list {
25+
for volume in &state.volume_list[range] {
2126
list = list.push(volume_row(volume, &state.pending_remove));
2227
}
23-
scrollable_list(list.into())
28+
paginated_list(list.into(), page, total)
2429
}
2530

2631
fn volume_row<'a>(

‎src/app/popup/widgets.rs‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use super::Message;
1010
use super::layout::{
1111
FILL_PORTION_LABELS, FILL_PORTION_PROMPT, FONT_PRIMARY, FONT_SECONDARY, FONT_STATUS, ICON_SIZE,
1212
PAD_CONFIRM_BUTTON, PAD_ICON_BUTTON, PAD_LIST_ROW, PAD_SECTION_DIVIDER, PAD_STATUS, PAD_TAB,
13-
POPUP_HEIGHT, POPUP_WIDTH, SPACING_MD, SPACING_SM, SPACING_XXS,
13+
POPUP_HEIGHT, POPUP_WIDTH, SPACING_MD, SPACING_NONE, SPACING_SM, SPACING_XXS,
1414
};
1515

1616
pub(super) fn popup_shell<'a>(
@@ -121,6 +121,60 @@ pub(super) fn scrollable_list<'a>(list: Element<'a, Message>) -> Element<'a, Mes
121121
.into()
122122
}
123123

124+
pub(super) fn paginated_list<'a>(
125+
list: Element<'a, Message>,
126+
current_page: usize,
127+
total_pages: usize,
128+
) -> Element<'a, Message> {
129+
widget::column![
130+
scrollable_list(list),
131+
pagination_bar(current_page, total_pages),
132+
]
133+
.spacing(SPACING_NONE)
134+
.height(Length::Fill)
135+
.into()
136+
}
137+
138+
pub(super) fn pagination_bar<'a>(current_page: usize, total_pages: usize) -> Element<'a, Message> {
139+
let page_label = format!("Page {} / {total_pages}", current_page + 1);
140+
let at_first = current_page == 0;
141+
let at_last = current_page + 1 >= total_pages;
142+
143+
let prev: Element<'a, Message> = if at_first {
144+
widget::button::text("‹").padding(PAD_TAB).into()
145+
} else {
146+
widget::button::text("‹")
147+
.padding(PAD_TAB)
148+
.class(theme::Button::Text)
149+
.on_press(Message::TabPagePrev)
150+
.into()
151+
};
152+
153+
let next: Element<'a, Message> = if at_last {
154+
widget::button::text("›").padding(PAD_TAB).into()
155+
} else {
156+
widget::button::text("›")
157+
.padding(PAD_TAB)
158+
.class(theme::Button::Text)
159+
.on_press(Message::TabPageNext)
160+
.into()
161+
};
162+
163+
widget::container(
164+
widget::row![
165+
prev,
166+
widget::text(page_label).size(FONT_STATUS),
167+
widget::text("").width(Length::Fill),
168+
next,
169+
]
170+
.align_y(Alignment::Center)
171+
.spacing(SPACING_SM),
172+
)
173+
.width(Length::Fill)
174+
.padding(PAD_STATUS)
175+
.into()
176+
}
177+
124178
pub(super) fn remove_confirm_row<'a>(prompt: String) -> Element<'a, Message> {
125179
widget::container(
126180
widget::row![

0 commit comments

Comments
 (0)