Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Replace page size by render height
Merged did:key:z6MkgFq6...nBGz opened 1 year ago
12 files changed +166 -295 c2b3a874 6484ede2
modified bin/commands/inbox/select.rs
@@ -64,9 +64,9 @@ pub enum AppPage {
pub struct BrowserState {
    items: Vec<NotificationItem>,
    selected: Option<usize>,
+
    scroll: usize,
    filter: NotificationItemFilter,
    search: store::StateValue<String>,
-
    page_size: usize,
    show_search: bool,
}

@@ -82,8 +82,7 @@ impl BrowserState {

#[derive(Clone, Debug)]
pub struct HelpState {
-
    progress: usize,
-
    page_size: usize,
+
    scroll: usize,
}

#[derive(Clone, Debug)]
@@ -192,31 +191,35 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items: notifications,
                selected: Some(0),
+
                scroll: 0,
                filter,
                search,
                show_search: false,
-
                page_size: 1,
-
            },
-
            help: HelpState {
-
                progress: 0,
-
                page_size: 1,
            },
+
            help: HelpState { scroll: 0 },
        })
    }
}

pub enum Message {
-
    Exit { selection: Option<Selection> },
-
    Select { selected: Option<usize> },
-
    BrowserPageSize(usize),
-
    HelpPageSize(usize),
+
    Exit {
+
        selection: Option<Selection>,
+
    },
+
    Select {
+
        selected: Option<usize>,
+
        scroll: usize,
+
    },
    OpenSearch,
-
    UpdateSearch { value: String },
+
    UpdateSearch {
+
        value: String,
+
    },
    ApplySearch,
    CloseSearch,
    OpenHelp,
    LeavePage,
-
    ScrollHelp { progress: usize },
+
    ScrollHelp {
+
        scroll: usize,
+
    },
}

impl store::State<Selection> for State {
@@ -225,16 +228,9 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected } => {
+
            Message::Select { selected, scroll } => {
                self.browser.selected = selected;
-
                None
-
            }
-
            Message::BrowserPageSize(size) => {
-
                self.browser.page_size = size;
-
                None
-
            }
-
            Message::HelpPageSize(size) => {
-
                self.help.page_size = size;
+
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -252,11 +248,13 @@ impl store::State<Selection> for State {
                    }
                }

+
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
+
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
@@ -275,8 +273,8 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { progress } => {
-
                self.help.progress = progress;
+
            Message::ScrollHelp { scroll } => {
+
                self.help.scroll = scroll;
                None
            }
        }
@@ -360,23 +358,10 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
        })
        .on_update(|state: &State| {
            PageProps::default()
-
                .page_size(state.browser.page_size)
                .handle_keys(!state.browser.show_search)
                .to_boxed_any()
                .into()
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::BrowserPageSize(page_size));
-
            }
-
            None
-
        })
}

fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
@@ -394,13 +379,12 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
                    Some(Message::ScrollHelp {
-
                        progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
+
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                    })
                })
-
                .on_update(|state: &State| {
+
                .on_update(|_| {
                    TextAreaProps::default()
                        .text(&help_text())
-
                        .page_size(state.help.page_size)
                        .to_boxed_any()
                        .into()
                }),
@@ -414,7 +398,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                            [
                                Column::new(Text::raw(""), Constraint::Fill(1)),
                                Column::new(
-
                                    span::default(&format!("{}%", state.help.progress)).dim(),
+
                                    span::default(&format!("{}%", state.help.scroll)).dim(),
                                    Constraint::Min(4),
                                ),
                            ]
@@ -442,25 +426,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            Key::Char('?') => Some(Message::LeavePage),
            _ => None,
        })
-
        .on_update(|state: &State| {
-
            PageProps::default()
-
                .page_size(state.help.page_size)
-
                .handle_keys(true)
-
                .to_boxed_any()
-
                .into()
-
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::HelpPageSize(page_size));
-
            }
-
            None
-
        })
+
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
}

fn help_text() -> Text<'static> {
modified bin/commands/inbox/select/ui.rs
@@ -18,7 +18,6 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps};
-
use tui::ui::widget::utils;
use tui::ui::widget::{self, ViewProps};
use tui::ui::widget::{RenderProps, ToWidget, View};

@@ -41,12 +40,12 @@ pub struct BrowserProps<'a> {
    notifications: Vec<NotificationItem>,
    /// Current (selected) table index
    selected: Option<usize>,
+
    /// Current scroll progress
+
    progress: usize,
    /// Notification statistics.
    stats: HashMap<String, usize>,
    /// Table columns
    columns: Vec<Column<'a>>,
-
    /// Current page size (height of table content).
-
    page_size: usize,
    /// If search widget should be shown.
    show_search: bool,
    /// Current search string.
@@ -80,6 +79,7 @@ impl<'a> From<&State> for BrowserProps<'a> {
            header,
            notifications,
            selected: state.browser.selected,
+
            progress: state.browser.scroll,
            stats,
            columns: [
                Column::new("", Constraint::Length(5)),
@@ -94,7 +94,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("", Constraint::Length(18)).hide_small(),
            ]
            .to_vec(),
-
            page_size: state.browser.page_size,
            search: state.browser.search.read(),
            show_search: state.browser.show_search,
        }
@@ -130,8 +129,11 @@ impl Browser {
                    Table::<State, Message, NotificationItem, 9>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
+
                            let (selected, scroll) =
+
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
-
                                selected: s.and_then(|s| s.unwrap_usize()),
+
                                selected: Some(selected),
+
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -142,7 +144,6 @@ impl Browser {
                                .items(state.browser.notifications())
                                .selected(state.browser.selected)
                                .footer(!state.browser.show_search)
-
                                .page_size(state.browser.page_size)
                                .to_boxed_any()
                                .into()
                        }),
@@ -288,17 +289,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
        span::default(" Unseen").dim(),
    ]);

-
    let progress = props
-
        .selected
-
        .map(|selected| {
-
            utils::scroll::percent_absolute(
-
                selected.saturating_sub(props.page_size),
-
                props.notifications.len(),
-
                props.page_size,
-
            )
-
        })
-
        .unwrap_or_default();
-
    let progress = span::default(&format!("{}%", progress)).dim();
+
    let progress = span::default(&format!("{}%", props.progress)).dim();

    match NotificationItemFilter::from_str(&props.search)
        .unwrap_or_default()
modified bin/commands/issue/select.rs
@@ -56,10 +56,11 @@ pub enum AppPage {
#[derive(Clone, Debug)]
pub struct BrowserState {
    items: Vec<IssueItem>,
+
    scroll: usize,
    selected: Option<usize>,
    filter: IssueItemFilter,
    search: store::StateValue<String>,
-
    page_size: usize,
+

    show_search: bool,
}

@@ -75,8 +76,7 @@ impl BrowserState {

#[derive(Clone, Debug)]
pub struct HelpState {
-
    progress: usize,
-
    page_size: usize,
+
    scroll: usize,
}

#[derive(Clone, Debug)]
@@ -110,31 +110,35 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items,
                selected: Some(0),
+
                scroll: 0,
                filter,
                search,
                show_search: false,
-
                page_size: 1,
-
            },
-
            help: HelpState {
-
                progress: 0,
-
                page_size: 1,
            },
+
            help: HelpState { scroll: 0 },
        })
    }
}

pub enum Message {
-
    Exit { selection: Option<Selection> },
-
    Select { selected: Option<usize> },
-
    BrowserPageSize(usize),
-
    HelpPageSize(usize),
+
    Exit {
+
        selection: Option<Selection>,
+
    },
+
    Select {
+
        selected: Option<usize>,
+
        scroll: usize,
+
    },
    OpenSearch,
-
    UpdateSearch { value: String },
+
    UpdateSearch {
+
        value: String,
+
    },
    ApplySearch,
    CloseSearch,
    OpenHelp,
    LeavePage,
-
    ScrollHelp { progress: usize },
+
    ScrollHelp {
+
        scroll: usize,
+
    },
}

impl store::State<Selection> for State {
@@ -143,16 +147,9 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected } => {
+
            Message::Select { selected, scroll } => {
                self.browser.selected = selected;
-
                None
-
            }
-
            Message::BrowserPageSize(size) => {
-
                self.browser.page_size = size;
-
                None
-
            }
-
            Message::HelpPageSize(size) => {
-
                self.help.page_size = size;
+
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -170,11 +167,13 @@ impl store::State<Selection> for State {
                    }
                }

+
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
+
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
@@ -193,8 +192,8 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { progress } => {
-
                self.help.progress = progress;
+
            Message::ScrollHelp { scroll } => {
+
                self.help.scroll = scroll;
                None
            }
        }
@@ -278,23 +277,10 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
        })
        .on_update(|state: &State| {
            PageProps::default()
-
                .page_size(state.browser.page_size)
                .handle_keys(!state.browser.show_search)
                .to_boxed_any()
                .into()
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::BrowserPageSize(page_size));
-
            }
-
            None
-
        })
}

fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
@@ -312,13 +298,12 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
                    Some(Message::ScrollHelp {
-
                        progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
+
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                    })
                })
-
                .on_update(|state: &State| {
+
                .on_update(|_| {
                    TextAreaProps::default()
                        .text(&help_text())
-
                        .page_size(state.help.page_size)
                        .to_boxed_any()
                        .into()
                }),
@@ -332,7 +317,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                            [
                                Column::new(Text::raw(""), Constraint::Fill(1)),
                                Column::new(
-
                                    span::default(&format!("{}%", state.help.progress)).dim(),
+
                                    span::default(&format!("{}%", state.help.scroll)).dim(),
                                    Constraint::Min(4),
                                ),
                            ]
@@ -360,25 +345,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            Key::Char('?') => Some(Message::LeavePage),
            _ => None,
        })
-
        .on_update(|state: &State| {
-
            PageProps::default()
-
                .page_size(state.help.page_size)
-
                .handle_keys(true)
-
                .to_boxed_any()
-
                .into()
-
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::HelpPageSize(page_size));
-
            }
-
            None
-
        })
+
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
}

fn help_text() -> Text<'static> {
modified bin/commands/issue/select/ui.rs
@@ -21,7 +21,6 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps};
-
use tui::ui::widget::utils;
use tui::ui::widget::ViewProps;
use tui::ui::widget::{RenderProps, ToWidget, View};

@@ -43,14 +42,14 @@ pub struct BrowserProps<'a> {
    issues: Vec<IssueItem>,
    /// Current (selected) table index
    selected: Option<usize>,
+
    /// Current scroll progress
+
    progress: usize,
    /// Issue statistics.
    stats: HashMap<String, usize>,
    /// Header columns
    header: Vec<Column<'a>>,
    /// Table columns
    columns: Vec<Column<'a>>,
-
    /// Current page size (height of table content).
-
    page_size: usize,
    /// If search widget should be shown.
    show_search: bool,
    /// Current search string.
@@ -115,7 +114,7 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("Opened", Constraint::Length(16)).hide_small(),
            ]
            .to_vec(),
-
            page_size: state.browser.page_size,
+
            progress: state.browser.scroll,
            search: state.browser.search.read(),
            show_search: state.browser.show_search,
        }
@@ -145,8 +144,11 @@ impl Browser {
                    Table::<State, Message, IssueItem, 8>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
+
                            let (selected, scroll) =
+
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
-
                                selected: s.and_then(|s| s.unwrap_usize()),
+
                                selected: Some(selected),
+
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -157,7 +159,6 @@ impl Browser {
                                .items(state.browser.issues())
                                .selected(state.browser.selected)
                                .footer(!state.browser.show_search)
-
                                .page_size(state.browser.page_size)
                                .to_boxed_any()
                                .into()
                        }),
@@ -166,7 +167,7 @@ impl Browser {
                    let props = BrowserProps::from(state);

                    FooterProps::default()
-
                        .columns(browse_footer(&props, props.selected))
+
                        .columns(browse_footer(&props))
                        .to_boxed_any()
                        .into()
                }))
@@ -286,7 +287,7 @@ impl View for Browser {
    }
}

-
fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
fn browse_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
    let search = Line::from(vec![
        span::default(" Search ").cyan().dim().reversed(),
        span::default(" "),
@@ -314,16 +315,7 @@ fn browse_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<C
        span::default(&props.issues.len().to_string()).dim(),
    ]);

-
    let progress = selected
-
        .map(|selected| {
-
            utils::scroll::percent_absolute(
-
                selected.saturating_sub(props.page_size),
-
                props.issues.len(),
-
                props.page_size,
-
            )
-
        })
-
        .unwrap_or_default();
-
    let progress = span::default(&format!("{}%", progress)).dim();
+
    let progress = span::default(&format!("{}%", props.progress)).dim();

    match IssueItemFilter::from_str(&props.search)
        .unwrap_or_default()
modified bin/commands/patch/select.rs
@@ -55,9 +55,9 @@ pub enum AppPage {
pub struct BrowserState {
    items: Vec<PatchItem>,
    selected: Option<usize>,
+
    scroll: usize,
    filter: PatchItemFilter,
    search: store::StateValue<String>,
-
    page_size: usize,
    show_search: bool,
}

@@ -73,8 +73,7 @@ impl BrowserState {

#[derive(Clone, Debug)]
pub struct HelpState {
-
    progress: usize,
-
    page_size: usize,
+
    scroll: usize,
}

#[derive(Clone, Debug)]
@@ -108,31 +107,35 @@ impl TryFrom<&Context> for State {
            browser: BrowserState {
                items,
                selected: Some(0),
+
                scroll: 0,
                filter,
                search,
                show_search: false,
-
                page_size: 1,
-
            },
-
            help: HelpState {
-
                progress: 0,
-
                page_size: 1,
            },
+
            help: HelpState { scroll: 0 },
        })
    }
}

pub enum Message {
-
    Exit { selection: Option<Selection> },
-
    Select { selected: Option<usize> },
-
    BrowserPageSize(usize),
-
    HelpPageSize(usize),
+
    Exit {
+
        selection: Option<Selection>,
+
    },
+
    Select {
+
        selected: Option<usize>,
+
        scroll: usize,
+
    },
    OpenSearch,
-
    UpdateSearch { value: String },
+
    UpdateSearch {
+
        value: String,
+
    },
    ApplySearch,
    CloseSearch,
    OpenHelp,
    LeavePage,
-
    ScrollHelp { progress: usize },
+
    ScrollHelp {
+
        scroll: usize,
+
    },
}

impl store::State<Selection> for State {
@@ -141,16 +144,9 @@ impl store::State<Selection> for State {
    fn update(&mut self, message: Message) -> Option<Exit<Selection>> {
        match message {
            Message::Exit { selection } => Some(Exit { value: selection }),
-
            Message::Select { selected } => {
+
            Message::Select { selected, scroll } => {
                self.browser.selected = selected;
-
                None
-
            }
-
            Message::BrowserPageSize(size) => {
-
                self.browser.page_size = size;
-
                None
-
            }
-
            Message::HelpPageSize(size) => {
-
                self.help.page_size = size;
+
                self.browser.scroll = scroll;
                None
            }
            Message::OpenSearch => {
@@ -168,11 +164,13 @@ impl store::State<Selection> for State {
                    }
                }

+
                self.browser.scroll = 0;
                None
            }
            Message::ApplySearch => {
                self.browser.search.apply();
                self.browser.show_search = false;
+
                self.browser.scroll = 0;
                None
            }
            Message::CloseSearch => {
@@ -191,8 +189,8 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { progress } => {
-
                self.help.progress = progress;
+
            Message::ScrollHelp { scroll } => {
+
                self.help.scroll = scroll;
                None
            }
        }
@@ -277,23 +275,10 @@ fn browser_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Mes
        })
        .on_update(|state: &State| {
            PageProps::default()
-
                .page_size(state.browser.page_size)
                .handle_keys(!state.browser.show_search)
                .to_boxed_any()
                .into()
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::BrowserPageSize(page_size));
-
            }
-
            None
-
        })
}

fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Message> {
@@ -311,13 +296,12 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
                    Some(Message::ScrollHelp {
-
                        progress: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
+
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
                    })
                })
-
                .on_update(|state: &State| {
+
                .on_update(|_| {
                    TextAreaProps::default()
                        .text(&help_text())
-
                        .page_size(state.help.page_size)
                        .to_boxed_any()
                        .into()
                }),
@@ -331,7 +315,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
                            [
                                Column::new(Text::raw(""), Constraint::Fill(1)),
                                Column::new(
-
                                    span::default(&format!("{}%", state.help.progress)).dim(),
+
                                    span::default(&format!("{}%", state.help.scroll)).dim(),
                                    Constraint::Min(4),
                                ),
                            ]
@@ -359,25 +343,7 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            Key::Char('?') => Some(Message::LeavePage),
            _ => None,
        })
-
        .on_update(|state: &State| {
-
            PageProps::default()
-
                .page_size(state.help.page_size)
-
                .handle_keys(true)
-
                .to_boxed_any()
-
                .into()
-
        })
-
        .on_render(|props, render| {
-
            let default = PageProps::default();
-
            let props = props
-
                .and_then(|props| props.inner_ref::<PageProps>())
-
                .unwrap_or(&default);
-
            let page_size = render.area.height.saturating_sub(6) as usize;
-

-
            if page_size != props.page_size {
-
                return Some(Message::HelpPageSize(page_size));
-
            }
-
            None
-
        })
+
        .on_update(|_| PageProps::default().handle_keys(true).to_boxed_any().into())
}

fn help_text() -> Text<'static> {
modified bin/commands/patch/select/ui.rs
@@ -23,7 +23,6 @@ use tui::ui::widget::container::{
};
use tui::ui::widget::input::{TextField, TextFieldProps};
use tui::ui::widget::list::{Table, TableProps};
-
use tui::ui::widget::utils;
use tui::ui::widget::ViewProps;
use tui::ui::widget::{RenderProps, ToWidget, View};

@@ -45,14 +44,14 @@ pub struct BrowserProps<'a> {
    patches: Vec<PatchItem>,
    /// Current (selected) table index
    selected: Option<usize>,
+
    /// Current scroll progress
+
    progress: usize,
    /// Patch statistics.
    stats: HashMap<String, usize>,
    /// Header columns
    header: Vec<Column<'a>>,
    /// Table columns
    columns: Vec<Column<'a>>,
-
    /// Current page size (height of table content).
-
    page_size: usize,
    /// If search widget should be shown.
    show_search: bool,
    /// Current search string.
@@ -91,6 +90,7 @@ impl<'a> From<&State> for BrowserProps<'a> {
            mode: state.mode.clone(),
            patches,
            selected: state.browser.selected,
+
            progress: state.browser.scroll,
            stats,
            header: [
                Column::new(" ● ", Constraint::Length(3)),
@@ -116,7 +116,6 @@ impl<'a> From<&State> for BrowserProps<'a> {
                Column::new("Updated", Constraint::Length(16)).hide_small(),
            ]
            .to_vec(),
-
            page_size: state.browser.page_size,
            show_search: state.browser.show_search,
            search: state.browser.search.read(),
        }
@@ -146,8 +145,11 @@ impl Browser {
                    Table::<State, Message, PatchItem, 9>::default()
                        .to_widget(tx.clone())
                        .on_event(|_, s, _| {
+
                            let (selected, scroll) =
+
                                s.and_then(|s| s.unwrap_table()).unwrap_or_default();
                            Some(Message::Select {
-
                                selected: s.and_then(|s| s.unwrap_usize()),
+
                                selected: Some(selected),
+
                                scroll,
                            })
                        })
                        .on_update(|state| {
@@ -158,7 +160,6 @@ impl Browser {
                                .items(state.browser.patches())
                                .selected(state.browser.selected)
                                .footer(!state.browser.show_search)
-
                                .page_size(state.browser.page_size)
                                .to_boxed_any()
                                .into()
                        }),
@@ -168,7 +169,7 @@ impl Browser {
                    let props = BrowserProps::from(state);

                    FooterProps::default()
-
                        .columns(browser_footer(&props, props.selected))
+
                        .columns(browser_footer(&props))
                        .to_boxed_any()
                        .into()
                }))
@@ -300,7 +301,7 @@ impl View for Browser {
    }
}

-
fn browser_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<Column<'a>> {
+
fn browser_footer<'a>(props: &BrowserProps<'a>) -> Vec<Column<'a>> {
    let filter = PatchItemFilter::from_str(&props.search).unwrap_or_default();

    let search = Line::from(vec![
@@ -338,16 +339,7 @@ fn browser_footer<'a>(props: &BrowserProps<'a>, selected: Option<usize>) -> Vec<
        span::default(&props.patches.len().to_string()).dim(),
    ]);

-
    let progress = selected
-
        .map(|selected| {
-
            utils::scroll::percent_absolute(
-
                selected.saturating_sub(props.page_size),
-
                props.patches.len(),
-
                props.page_size,
-
            )
-
        })
-
        .unwrap_or_default();
-
    let progress = span::default(&format!("{}%", progress)).dim();
+
    let progress = span::default(&format!("{}%", props.progress)).dim();

    match filter.status() {
        Some(state) => {
modified src/ui/widget.rs
@@ -63,6 +63,7 @@ impl From<&'static dyn Any> for ViewProps {
pub enum ViewState {
    USize(usize),
    String(String),
+
    Table { selected: usize, scroll: usize },
}

impl ViewState {
@@ -79,6 +80,13 @@ impl ViewState {
            _ => None,
        }
    }
+

+
    pub fn unwrap_table(&self) -> Option<(usize, usize)> {
+
        match self {
+
            ViewState::Table { selected, scroll } => Some((*selected, *scroll)),
+
            _ => None,
+
        }
+
    }
}

#[derive(Clone, Default)]
modified src/ui/widget/container.rs
@@ -12,7 +12,7 @@ use crate::ui::{RENDER_WIDTH_LARGE, RENDER_WIDTH_MEDIUM, RENDER_WIDTH_SMALL};

use super::{PredefinedLayout, RenderProps, View, ViewProps, Widget};

-
#[derive(Clone, Debug)]
+
#[derive(Clone, Debug, Default)]
pub struct ColumnView {
    small: bool,
    medium: bool,
@@ -44,16 +44,6 @@ impl ColumnView {
    }
}

-
impl Default for ColumnView {
-
    fn default() -> Self {
-
        Self {
-
            small: false,
-
            medium: false,
-
            large: false,
-
        }
-
    }
-
}
-

#[derive(Clone, Debug)]
pub struct Column<'a> {
    pub text: Text<'a>,
modified src/ui/widget/list.rs
@@ -14,7 +14,7 @@ use crate::ui::theme::style;
use crate::ui::{layout, span};

use super::{container::Column, RenderProps, View};
-
use super::{ViewProps, ViewState};
+
use super::{utils, ViewProps, ViewState};

/// Needs to be implemented for items that are supposed to be rendered in tables.
pub trait ToRow<const W: usize> {
@@ -30,7 +30,6 @@ where
    pub selected: Option<usize>,
    pub columns: Vec<Column<'a>>,
    pub has_footer: bool,
-
    pub page_size: usize,
}

impl<'a, R, const W: usize> Default for TableProps<'a, R, W>
@@ -42,7 +41,6 @@ where
            items: vec![],
            columns: vec![],
            has_footer: false,
-
            page_size: 1,
            selected: Some(0),
        }
    }
@@ -71,11 +69,6 @@ where
        self.has_footer = has_footer;
        self
    }
-

-
    pub fn page_size(mut self, page_size: usize) -> Self {
-
        self.page_size = page_size;
-
        self
-
    }
}

pub struct Table<S, M, R, const W: usize>
@@ -83,9 +76,11 @@ where
    R: ToRow<W>,
{
    /// Internal selection and offset state
-
    state: TableState,
+
    state: (TableState, usize),
    /// Phantom
    phantom: PhantomData<(S, M, R)>,
+
    /// Current render height
+
    height: u16,
}

impl<S, M, R, const W: usize> Default for Table<S, M, R, W>
@@ -94,8 +89,9 @@ where
{
    fn default() -> Self {
        Self {
-
            state: TableState::default().with_selected(Some(0)),
+
            state: (TableState::default().with_selected(Some(0)), 0),
            phantom: PhantomData,
+
            height: 1,
        }
    }
}
@@ -107,51 +103,53 @@ where
    fn prev(&mut self) -> Option<usize> {
        let selected = self
            .state
+
            .0
            .selected()
            .map(|current| current.saturating_sub(1));
-
        self.state.select(selected);
+
        self.state.0.select(selected);
        selected
    }

    fn next(&mut self, len: usize) -> Option<usize> {
-
        let selected = self.state.selected().map(|current| {
+
        let selected = self.state.0.selected().map(|current| {
            if current < len.saturating_sub(1) {
                current.saturating_add(1)
            } else {
                current
            }
        });
-
        self.state.select(selected);
+
        self.state.0.select(selected);
        selected
    }

    fn prev_page(&mut self, page_size: usize) -> Option<usize> {
        let selected = self
            .state
+
            .0
            .selected()
            .map(|current| current.saturating_sub(page_size));
-
        self.state.select(selected);
+
        self.state.0.select(selected);
        selected
    }

    fn next_page(&mut self, len: usize, page_size: usize) -> Option<usize> {
-
        let selected = self.state.selected().map(|current| {
+
        let selected = self.state.0.selected().map(|current| {
            if current < len.saturating_sub(1) {
                cmp::min(current.saturating_add(page_size), len.saturating_sub(1))
            } else {
                current
            }
        });
-
        self.state.select(selected);
+
        self.state.0.select(selected);
        selected
    }

    fn begin(&mut self) {
-
        self.state.select(Some(0));
+
        self.state.0.select(Some(0));
    }

    fn end(&mut self, len: usize) {
-
        self.state.select(Some(len.saturating_sub(1)));
+
        self.state.0.select(Some(len.saturating_sub(1)));
    }
}

@@ -170,6 +168,8 @@ where
            .and_then(|props| props.inner_ref::<TableProps<R, W>>())
            .unwrap_or(&default);

+
        let page_size = self.height;
+

        match key {
            Key::Up | Key::Char('k') => {
                self.prev();
@@ -178,10 +178,10 @@ where
                self.next(props.items.len());
            }
            Key::PageUp => {
-
                self.prev_page(props.page_size);
+
                self.prev_page(page_size as usize);
            }
            Key::PageDown => {
-
                self.next_page(props.items.len(), props.page_size);
+
                self.next_page(props.items.len(), page_size as usize);
            }
            Key::Home => {
                self.begin();
@@ -201,9 +201,10 @@ where
            .and_then(|props| props.inner_ref::<TableProps<R, W>>())
            .unwrap_or(&default);

-
        if props.selected != self.state.selected() {
-
            self.state.select(props.selected);
+
        if props.selected != self.state.0.selected() {
+
            self.state.0.select(props.selected);
        }
+
        self.state.1 = props.items.len();
    }

    fn render(&mut self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
@@ -251,7 +252,7 @@ where
                .column_spacing(1)
                .highlight_style(style::highlight(render.focus));

-
            frame.render_stateful_widget(rows, render.area, &mut self.state);
+
            frame.render_stateful_widget(rows, render.area, &mut self.state.0);
        } else {
            let center = layout::centered_rect(render.area, 50, 10);
            let hint = Text::from(span::default("Nothing to show"))
@@ -261,9 +262,20 @@ where

            frame.render_widget(hint, center);
        }
+

+
        self.height = render.area.height;
    }

    fn view_state(&self) -> Option<ViewState> {
-
        self.state.selected().map(ViewState::USize)
+
        let selected = self.state.0.selected().unwrap_or_default();
+

+
        Some(ViewState::Table {
+
            selected,
+
            scroll: utils::scroll::percent_absolute(
+
                selected.saturating_sub(self.height.into()),
+
                self.state.1,
+
                self.height.into(),
+
            ),
+
        })
    }
}
modified src/ui/widget/text.rs
@@ -14,17 +14,11 @@ pub struct TextAreaProps<'a> {
    pub content: Text<'a>,
    pub has_header: bool,
    pub has_footer: bool,
-
    pub page_size: usize,
    pub progress: usize,
    pub can_scroll: bool,
}

impl<'a> TextAreaProps<'a> {
-
    pub fn page_size(mut self, page_size: usize) -> Self {
-
        self.page_size = page_size;
-
        self
-
    }
-

    pub fn text(mut self, text: &Text<'a>) -> Self {
        self.content = text.clone();
        self
@@ -42,7 +36,6 @@ impl<'a> Default for TextAreaProps<'a> {
            content: Text::raw(""),
            has_header: false,
            has_footer: false,
-
            page_size: 1,
            progress: 0,
            can_scroll: true,
        }
@@ -62,6 +55,8 @@ pub struct TextArea<S, M> {
    state: TextAreaState,
    /// Phantom
    phantom: PhantomData<(S, M)>,
+
    /// Current render height
+
    height: u16,
}

impl<S, M> Default for TextArea<S, M> {
@@ -72,6 +67,7 @@ impl<S, M> Default for TextArea<S, M> {
                progress: 0,
            },
            phantom: PhantomData,
+
            height: 1,
        }
    }
}
@@ -139,7 +135,7 @@ where
            .unwrap_or(&default);

        let len = props.content.lines.len() + 1;
-
        let page_size = props.page_size;
+
        let page_size = self.height as usize;

        if props.can_scroll {
            match key {
@@ -169,6 +165,8 @@ where
    }

    fn render(&mut self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        self.height = render.area.height;
+

        let default = TextAreaProps::default();
        let props = props
            .and_then(|props| props.inner_ref::<TextAreaProps>())
modified src/ui/widget/utils.rs
@@ -15,16 +15,12 @@ pub mod scroll {
    }

    pub fn percent_absolute(offset: usize, len: usize, height: usize) -> usize {
-
        if height >= len {
-
            100
-
        } else {
-
            let y = offset as f64;
-
            let h = height as f64;
-
            let t = len.saturating_sub(1) as f64;
-
            let v = y / (t - h) * 100_f64;
+
        let y = offset as f64;
+
        let h = height as f64;
+
        let t = len.saturating_sub(1) as f64;
+
        let v = y / (t - h) * 100_f64;

-
            std::cmp::max(0, std::cmp::min(100, v as usize))
-
        }
+
        std::cmp::max(0, std::cmp::min(100, v as usize))
    }

    fn map_range(from: (f64, f64), to: (f64, f64), value: f64) -> f64 {
modified src/ui/widget/window.rs
@@ -119,18 +119,11 @@ where

#[derive(Clone, Default)]
pub struct PageProps {
-
    /// Current page size (height of table content etc.).
-
    pub page_size: usize,
    /// If this view's should handle keys
    pub handle_keys: bool,
}

impl PageProps {
-
    pub fn page_size(mut self, page_size: usize) -> Self {
-
        self.page_size = page_size;
-
        self
-
    }
-

    pub fn handle_keys(mut self, handle_keys: bool) -> Self {
        self.handle_keys = handle_keys;
        self