add error report view

[?]
Sep 10, 2025, 7:30 PM
CULHFNIVQ3ATML2W3Z45RARZ2LHGXONYTGGN2ETWAAMV7R3Y67AQC

Dependencies

  • [2] 6YZAVBWU Initial commit
  • [3] AMPZ2BXK show changed files diffs (only Edit atm)
  • [4] 23SFYK4Q big view refactor into a new crate
  • [5] OPXFZKEB view tests setup
  • [6] 3QVNMRNM test non-empty repo app view
  • [7] MYGIBRRH wip custom theme
  • [8] PKJCFSBM theme improvements
  • [9] XSZZB47U refactor stuff into lib
  • [10] XIASAP3G clippy
  • [11] 3BK22XE5 add a test for hover btn and more refactors
  • [12] ZD56BUSU add back +/- bg colors
  • [13] I2AG42PA new cols layout
  • [14] SASAN2XC use nav-scrollable
  • [15] UR4J677R nav for log changes and refactors
  • [16] A6Z4O6RC actions menu
  • [17] JZXYSIYD channel selection!
  • [18] 5ZRDYL6K fork channel, fix recording esc key
  • [19] BAUK5BON pimp-up action buttons
  • [20] FJSVMFB4 add `iced_expl_widgets` with forked scrollable
  • [21] 3XRG4BB6 rewritten nav-scrollable!
  • [22] WAOGSCOJ very nice refactor, wip adding channels logs
  • [23] WH57EHNM update tests
  • [24] EJPSD5XO shared allowed actions conditions between update and view
  • [25] YK3MOJJL chonky refactor, wip other channels logs & diffs
  • [26] 7WCB5YQJ refactor msgs and modules
  • [27] AZ5D2LQU allow to set record description
  • [28] PKLUHYE4 allow to copy change hash
  • [29] YKHE3XMW refactor diffs handling
  • [30] ZIUHKVJK update tests
  • [31] KEPKF3WO unify diffs handling, simplify view
  • [32] PTWZYQFR use nav-scrollable for repo status

Change contents

  • replacement in inflorescence_view/src/view.rs at line 7
    [22.3467][21.2426:2464](),[17.2990][21.2426:2464]()
    use iced_expl_widget::nav_scrollable;
    [22.3467]
    [27.937]
    use iced_expl_widget::{nav_scrollable, report};
  • replacement in inflorescence_view/src/view.rs at line 16
    [18.2816][17.2991:3060](),[15.1090][17.2991:3060]()
    use iced::{alignment, font, window, Element, Font, Length, Padding};
    [18.2816]
    [4.12673]
    use iced::{
    alignment, font, window, Alignment, Element, Font, Length, Padding,
    };
  • edit in inflorescence_view/src/view.rs at line 51
    [24.487]
    [15.2300]
    report,
  • edit in inflorescence_view/src/view.rs at line 54
    [22.3942]
    [22.3942]
    // TODO: show report in load and other states
  • edit in inflorescence_view/src/view.rs at line 69
    [24.653]
    [24.653]
    report,
  • edit in inflorescence_view/src/view.rs at line 86
    [24.706]
    [22.4380]
    report: &'a report::Container,
  • edit in inflorescence_view/src/view.rs at line 946
    [17.7502]
    [16.2241]
    // Overlay the main view with reports
    let main = report::view(report, main, |report| {
    let report_width =
    if window_size.width > DEFAULT_MIN_COL_WIDTH as f32 * 1.3 {
    window_size.width * 0.45
    } else {
    window_size.width * 0.95
    };
    let max_height = window_size.height * 0.85;
    el(container(column([
    el(row([el(container(
    button(text("↧").shaping(text::Shaping::Advanced)).on_press(
    Msg::Action(action::FilteredMsg::ToggleErrorReports),
    ),
    )
    .width(Length::Fill)
    .align_x(Alignment::End))])
    .width(Length::Fill)),
    el(
    container(column(report.entries.iter().map(view_report_entry)))
    .padding([4, 6])
    .class(theme::Container::Report),
    ),
    ]))
    .width(Length::Fixed(report_width))
    .height(Length::Shrink)
    .max_height(max_height))
    });
  • edit in inflorescence_view/src/view.rs at line 979
    [16.2274]
    [22.20173]
    }
    fn view_report_entry<'a>(entry: &'a report::Entry) -> Element<'a, Msg, Theme>
    where
    Msg: 'a,
    {
    let report::Entry {
    level,
    msg,
    time,
    is_read,
    } = entry;
    let time = time.strftime("%I:%M:%S.%3f").to_string();
    let text_class = if *is_read {
    theme::Text::SlightlyFaded
    } else {
    theme::Text::Normal
    };
    el(row([
    el(text(msg)
    .width(Length::Fill)
    .wrapping(text::Wrapping::WordOrGlyph)
    .class(text_class)),
    el(container(row([]))
    .width(Length::Fixed(8.0))
    .height(Length::Fill)
    .class(theme::Container::ReportLevel {
    level: *level,
    is_read: *is_read,
    })),
    el(text(time)
    .align_x(Alignment::End)
    .size(11.0)
    .class(text_class)),
    ])
    .spacing(6)
    .padding(4)
    .width(Length::Fill)
    .height(Length::Shrink))
  • edit in inflorescence_view/src/view/test.rs at line 6
    [5.4320]
    [26.3236]
    use iced_expl_widget::report;
  • edit in inflorescence_view/src/view/test.rs at line 17
    [9.5949]
    [13.4094]
    use std::str::FromStr;
  • replacement in inflorescence_view/src/view/test.rs at line 36
    [5.4685][5.4685:4725]()
    let uniq_name = "app_loading_repo";
    [5.4685]
    [5.4725]
    let uniq_name = "loading_repo";
  • edit in inflorescence_view/src/view/test.rs at line 47
    [25.16902]
    [5.5139]
    report: report::Container::default(),
  • replacement in inflorescence_view/src/view/test.rs at line 54
    [5.5363][5.5363:5408]()
    let uniq_name = "app_loaded_empty_repo";
    [5.5363]
    [5.5408]
    let uniq_name = "loaded_empty_repo";
  • edit in inflorescence_view/src/view/test.rs at line 78
    [25.16936]
    [6.289]
    report: report::Container::default(),
  • replacement in inflorescence_view/src/view/test.rs at line 85
    [6.513][6.513:562]()
    let uniq_name = "app_loaded_non_empty_repo";
    [6.513]
    [6.562]
    let uniq_name = "loaded_non_empty_repo";
  • edit in inflorescence_view/src/view/test.rs at line 123
    [25.16970]
    [7.8460]
    report: report::Container::default(),
  • replacement in inflorescence_view/src/view/test.rs at line 130
    [11.1229][11.1229:1282]()
    let uniq_name = "app_loaded_point_at_untracked";
    [11.1229]
    [11.1282]
    let uniq_name = "loaded_point_at_untracked";
  • replacement in inflorescence_view/src/view/test.rs at line 132
    [11.1283][23.1814:1892]()
    test_view_change_sim(&mut results, uniq_name, view(&state), size, |sim| {
    [11.1283]
    [23.1892]
    test_view_change_sim(mresults, uniq_name, view(&state), size, |sim| {
  • replacement in inflorescence_view/src/view/test.rs at line 139
    [7.8684][7.8684:8737]()
    let uniq_name = "app_loaded_selected_untracked";
    [7.8684]
    [7.8737]
    let uniq_name = "loaded_selected_untracked";
  • edit in inflorescence_view/src/view/test.rs at line 163
    [25.17004]
    [5.5822]
    report: report::Container::default(),
  • replacement in inflorescence_view/src/view/test.rs at line 165
    [5.5829][23.2567:2627]()
    test_view(&mut results, uniq_name, view(&state), size);
    [5.5829]
    [5.5957]
    test_view(mresults, uniq_name, view(&state), size);
    // _________________________________________________________________________
    //
    let uniq_name = "loaded_shown_error_report";
    let repo = repo::State {
    dir_name: "path".to_string(),
    channel: "some_channel".to_string(),
    other_channels: vec![],
    untracked_files: BTreeSet::new(),
    changed_files: BTreeMap::new(),
    short_log: vec![],
    };
    let ready_state = ReadyState {
    user_id: Id::default().unwrap(),
    repo,
    selection: default(),
    navigation: default(),
    record_changes: default(),
    logs: default(),
    forking_channel_name: default(),
    };
    let repo_path = PathBuf::from("test/repo/path");
    let state = State {
    window_size: WINDOW_SIZE,
    repo_path: repo_path.clone(),
    sub: SubState::Ready(ready_state),
    allowed_actions: vec![],
    report: report::Container {
    hidden: false,
    entries: vec![
    report::Entry {
    level: report::Level::Error,
    msg: "Sed a rhoncus elit. Vestibulum molestie lacus blandit eleifend auctor. Nulla eget fermentum erat. Suspendisse quis erat faucibus enim facilisis cursus.".to_string(),
    time: Timestamp::from_str("2024-03-10T06:05:00Z").unwrap(),
    is_read: false,
    },
    report::Entry {
    level: report::Level::Warning,
    msg: "Morbi at justo".to_string(),
    time: Timestamp::from_str("2024-03-10T06:04:33Z").unwrap(),
    is_read: false,
    },
    report::Entry {
    level: report::Level::Error,
    msg: "Curabitur sit amet nisl venenatis, suscipit magna at, elementum dolor".to_string(),
    time: Timestamp::from_str("2024-03-10T06:02:10Z").unwrap(),
    is_read: true,
    },
    report::Entry {
    level: report::Level::Warning,
    msg: "Aliquam lobortis egestas diam, a luctus dui imperdiet vitae".to_string(),
    time: Timestamp::from_str("2024-03-10T06:00:00Z").unwrap(),
    is_read: true,
    },
    ],
    },
    };
  • edit in inflorescence_view/src/view/test.rs at line 225
    [5.5958]
    [5.5958]
    test_view(mresults, uniq_name, view(&state), size);
  • replacement in inflorescence_view/src/theme.rs at line 4
    [9.4555][21.22:60]()
    use iced_expl_widget::nav_scrollable;
    [9.4555]
    [7.168]
    use iced_expl_widget::{nav_scrollable, report};
  • edit in inflorescence_view/src/theme.rs at line 36
    [8.832]
    [8.832]
    const TURQOISE_DARKER: Color = color!(0x0CA592);
  • edit in inflorescence_view/src/theme.rs at line 39
    [8.874]
    [12.23]
    const TEXT_COLOR_FADED: Color = Color {
    a: 0.7,
    ..TEXT_COLOR
    };
  • edit in inflorescence_view/src/theme.rs at line 46
    [12.162]
    [8.874]
    const REPORT_ERROR_COLOR: Color = color!(0x9d140e);
    const REPORT_WARNING_COLOR: Color = color!(0x999208);
  • edit in inflorescence_view/src/theme.rs at line 98
    [12.199]
    [10.20]
    Report,
    ReportLevel { level: report::Level, is_read: bool },
  • edit in inflorescence_view/src/theme.rs at line 111
    [10.100]
    [19.22]
    SlightlyFaded,
  • edit in inflorescence_view/src/theme.rs at line 225
    [12.499]
    [12.499]
    ..default()
    },
    Container::Report => container::Style {
    background: Some(ACTIONS_BG),
    border: Border {
    color: TURQOISE_DARKER,
    width: 1.0,
    ..default()
    },
  • edit in inflorescence_view/src/theme.rs at line 236
    [12.542]
    [8.2391]
    Container::ReportLevel { level, is_read } => {
    let color = match level {
    report::Level::Warning => REPORT_WARNING_COLOR,
    report::Level::Error => REPORT_ERROR_COLOR,
    };
    let color = if *is_read {
    Color { a: 0.7, ..color }
    } else {
    color
    };
    container::Style {
    background: Some(Background::Color(color)),
    ..default()
    }
    }
  • edit in inflorescence_view/src/theme.rs at line 345
    [8.2530]
    [19.47]
    Text::SlightlyFaded => text::Style {
    color: Some(TEXT_COLOR_FADED),
    },
  • replacement in inflorescence_model/src/model.rs at line 3
    [26.3595][26.3595:3633]()
    use iced_expl_widget::nav_scrollable;
    [26.3595]
    [26.3633]
    use iced_expl_widget::{nav_scrollable, report};
  • edit in inflorescence_model/src/model.rs at line 33
    [26.4335]
    [26.4335]
    pub report: report::Container,
  • replacement in inflorescence_model/src/action.rs at line 3
    [26.7153][24.3704:3742](),[25.18831][24.3704:3742](),[24.3704][24.3704:3742]()
    use iced_expl_widget::nav_scrollable;
    [26.7153]
    [25.18832]
    use iced_expl_widget::{nav_scrollable, report};
  • edit in inflorescence_model/src/action.rs at line 34
    [28.205]
    [24.4320]
    ToggleErrorReports,
  • edit in inflorescence_model/src/action.rs at line 73
    [28.254]
    [24.5127]
    (ToggleErrorReports, ToggleErrorReports) => true,
  • edit in inflorescence_model/src/action.rs at line 90
    [28.292]
    [24.5587]
    (ToggleErrorReports, _) => false,
  • replacement in inflorescence_model/src/action.rs at line 94
    [24.5596][24.5596:5676]()
    pub fn get_allowed(state: &model::SubState) -> Vec<Binding> {
    match state {
    [24.5596]
    [24.5676]
    pub fn get_allowed(
    state: &model::SubState,
    report: &report::Container,
    ) -> Vec<Binding> {
    let mut bindings = match state {
  • edit in inflorescence_model/src/action.rs at line 102
    [24.5862]
    [24.5862]
    };
    if !report.entries.is_empty() {
    let label = if !report.hidden {
    "Hide errors"
    } else {
    "Show errors"
    };
    bindings.push(Binding {
    key: "C-e",
    label,
    msg: Some(FilteredMsg::ToggleErrorReports),
    });
  • edit in inflorescence_model/src/action.rs at line 116
    [24.5868]
    [24.5868]
    bindings
  • edit in inflorescence/src/main.rs at line 8
    [3.1220]
    [14.2336]
    use iced_expl_widget::report;
  • edit in inflorescence/src/main.rs at line 120
    [24.23774]
    [24.23774]
    report: report::Container::default(),
  • replacement in inflorescence/src/main.rs at line 185
    [24.24296][27.9874:9947]()
    state.model.allowed_actions = action::get_allowed(&state.model.sub);
    [24.24296]
    [24.24365]
    state.model.allowed_actions =
    action::get_allowed(&state.model.sub, &state.model.report);
  • edit in inflorescence/src/main.rs at line 485
    [28.2205]
    [25.60824]
    action::FilteredMsg::ToggleErrorReports => {
    state.model.report.hidden = !state.model.report.hidden;
    Task::none()
    }
  • edit in inflorescence/src/main.rs at line 1413
    [4.28349]
    [16.13284]
    "e" if mods == Modifiers::CTRL => {
    action(action::FilteredMsg::ToggleErrorReports)
    }
  • file addition: report.rs (----------)
    [20.713]
    //! Widget used to display timestamped error and warning reports in an overlay.
    use iced::advanced::{layout, overlay};
    use iced::{mouse, Alignment, Element, Event, Point, Rectangle, Size, Vector};
    use iced_core::widget::{Operation, Tree};
    use iced_core::{renderer, Clipboard, Layout, Shell};
    use jiff::Timestamp;
    #[derive(Clone, Copy, Debug)]
    pub enum Level {
    Warning,
    Error,
    }
    #[derive(Debug)]
    pub struct Entry {
    pub level: Level,
    pub msg: String,
    pub time: Timestamp,
    pub is_read: bool,
    }
    #[derive(Debug, Default)]
    pub struct Container {
    pub hidden: bool,
    pub entries: Vec<Entry>,
    }
    struct Widget<'a, Msg, Theme, Renderer> {
    pub overlay: Element<'a, Msg, Theme, Renderer>,
    pub overlaid: Element<'a, Msg, Theme, Renderer>,
    }
    pub fn view<'a, Msg, Theme, Renderer, FV>(
    container: &'a Container,
    overlaid: Element<'a, Msg, Theme, Renderer>,
    view_container: FV,
    ) -> Element<'a, Msg, Theme, Renderer>
    where
    Msg: 'a,
    Theme: 'a,
    Renderer: 'a + iced::advanced::Renderer,
    FV: Fn(&'a Container) -> Element<'a, Msg, Theme, Renderer>,
    {
    if container.hidden || container.entries.is_empty() {
    overlaid
    } else {
    let overlay = view_container(container);
    Element::new(Widget::<'a, Msg, _, _> { overlay, overlaid })
    }
    }
    impl<Msg, Theme, Renderer> iced::advanced::Widget<Msg, Theme, Renderer>
    for Widget<'_, Msg, Theme, Renderer>
    where
    Renderer: iced::advanced::Renderer,
    {
    fn size(&self) -> iced::Size<iced::Length> {
    self.overlaid.as_widget().size()
    }
    fn layout(
    &self,
    tree: &mut iced_core::widget::Tree,
    renderer: &Renderer,
    limits: &iced_core::layout::Limits,
    ) -> iced_core::layout::Node {
    self.overlaid.as_widget().layout(
    &mut tree.children[0],
    renderer,
    limits,
    )
    }
    fn update(
    &mut self,
    state: &mut Tree,
    event: &Event,
    layout: Layout<'_>,
    cursor: mouse::Cursor,
    renderer: &Renderer,
    clipboard: &mut dyn Clipboard,
    shell: &mut Shell<'_, Msg>,
    viewport: &Rectangle,
    ) {
    self.overlaid.as_widget_mut().update(
    &mut state.children[0],
    event,
    layout,
    cursor,
    renderer,
    clipboard,
    shell,
    viewport,
    );
    }
    fn draw(
    &self,
    tree: &iced_core::widget::Tree,
    renderer: &mut Renderer,
    theme: &Theme,
    style: &iced_core::renderer::Style,
    layout: iced_core::Layout<'_>,
    cursor: iced_core::mouse::Cursor,
    viewport: &iced::Rectangle,
    ) {
    self.overlaid.as_widget().draw(
    &tree.children[0],
    renderer,
    theme,
    style,
    layout,
    cursor,
    viewport,
    )
    }
    fn children(&self) -> Vec<Tree> {
    vec![Tree::new(&self.overlaid), Tree::new(&self.overlay)]
    }
    fn diff(&self, tree: &mut Tree) {
    tree.diff_children(&[&self.overlaid, &self.overlay]);
    }
    fn operate(
    &self,
    state: &mut Tree,
    layout: Layout<'_>,
    renderer: &Renderer,
    operation: &mut dyn Operation,
    ) {
    operation.container(None, layout.bounds(), &mut |operation| {
    self.overlaid.as_widget().operate(
    &mut state.children[0],
    layout,
    renderer,
    operation,
    );
    });
    }
    fn mouse_interaction(
    &self,
    state: &Tree,
    layout: Layout<'_>,
    cursor: mouse::Cursor,
    viewport: &Rectangle,
    renderer: &Renderer,
    ) -> mouse::Interaction {
    self.overlaid.as_widget().mouse_interaction(
    &state.children[0],
    layout,
    cursor,
    viewport,
    renderer,
    )
    }
    fn overlay<'a>(
    &'a mut self,
    tree: &'a mut Tree,
    layout: Layout<'a>,
    renderer: &Renderer,
    viewport: &Rectangle,
    translation: Vector,
    ) -> Option<overlay::Element<'a, Msg, Theme, Renderer>> {
    let (content_tree, toast_tree) = tree.children.split_at_mut(1);
    let content_overlay = self.overlaid.as_widget_mut().overlay(
    &mut content_tree[0],
    layout,
    renderer,
    viewport,
    translation,
    );
    let toast_overlay = Overlay {
    overlay: &mut self.overlay,
    tree: &mut toast_tree[0],
    position: layout.bounds().position() + translation,
    bounds: layout.bounds(),
    };
    let toast_overlay = overlay::Element::new(Box::new(toast_overlay));
    let overlays = content_overlay
    .into_iter()
    .chain(std::iter::once(toast_overlay))
    .collect::<Vec<_>>();
    Some(overlay::Group::with_children(overlays).overlay())
    }
    }
    struct Overlay<'a, 'b, Msg, Theme, Renderer> {
    overlay: &'b mut Element<'a, Msg, Theme, Renderer>,
    tree: &'b mut Tree,
    position: Point,
    bounds: Rectangle,
    }
    impl<'a, Msg, Theme, Renderer> overlay::Overlay<Msg, Theme, Renderer>
    for Overlay<'a, '_, Msg, Theme, Renderer>
    where
    Renderer: iced::advanced::Renderer,
    {
    fn layout(&mut self, renderer: &Renderer, _bounds: Size) -> layout::Node {
    let bounds = self.bounds.size();
    let limits = layout::Limits::new(Size::ZERO, bounds);
    layout::Node::with_children(
    bounds,
    vec![self
    .overlay
    .as_widget()
    .layout(self.tree, renderer, &limits)
    .align(
    // TODO parameterize
    Alignment::End,
    Alignment::End,
    bounds,
    )],
    )
    .translate([self.position.x, self.position.y])
    }
    fn draw(
    &self,
    renderer: &mut Renderer,
    theme: &Theme,
    style: &renderer::Style,
    layout: Layout<'_>,
    cursor: mouse::Cursor,
    ) {
    let viewport = layout.bounds();
    self.overlay.as_widget().draw(
    self.tree,
    renderer,
    theme,
    style,
    layout.children().next().unwrap(),
    cursor,
    &viewport,
    )
    }
    fn update(
    &mut self,
    event: &Event,
    layout: Layout<'_>,
    cursor: mouse::Cursor,
    renderer: &Renderer,
    clipboard: &mut dyn Clipboard,
    shell: &mut Shell<'_, Msg>,
    ) {
    let viewport = layout.bounds();
    self.overlay.as_widget_mut().update(
    self.tree,
    event,
    layout.children().next().unwrap(),
    cursor,
    renderer,
    clipboard,
    shell,
    &viewport,
    );
    }
    fn operate(
    &mut self,
    layout: Layout<'_>,
    renderer: &Renderer,
    operation: &mut dyn Operation,
    ) {
    operation.container(None, layout.bounds(), &mut |operation| {
    self.overlay.as_widget().operate(
    self.tree,
    layout.children().next().unwrap(),
    renderer,
    operation,
    );
    });
    }
    fn mouse_interaction(
    &self,
    layout: Layout<'_>,
    cursor: mouse::Cursor,
    renderer: &Renderer,
    ) -> mouse::Interaction {
    let viewport = layout.bounds();
    self.overlay.as_widget().mouse_interaction(
    self.tree,
    layout.children().next().unwrap(),
    cursor,
    &viewport,
    renderer,
    )
    }
    }
  • edit in iced_expl_widget/src/lib.rs at line 2
    [21.115267]
    [20.73917]
    pub mod report;
  • edit in iced_expl_widget/Cargo.toml at line 20
    [20.75521]
    [20.75521]
    workspace = true
    [dependencies.jiff]
  • edit in Cargo.lock at line 2105
    [21.116818]
    [2.39486]
    "jiff",