Iced + Canvas + Update Fn: Does canvas update if state does not actually change

I have an iced canvas program that displays a line graph. Below the graph are 4 text items that can be clicked which change the date range on the x axis and in turn updates the line graph accordingly. This is accomplished by testing for cursor clicks in bounding rectangles and if so, a message and value are sent to the main program and a state variable is updated. If I click the text that displays 90 days worth of data, the graph updates. If I click it again does the graph update again or when the state variable in the main program is updated does rust see that it the old value and new value are the same thus there is no true state change thus no redraw is required? If it does redraw again, should I trap the value in the main update function or in the canvas update function to keep the redraw from being completed? Below is my canvas update function code. Any assistance will be appreciated.

    fn update(
        &self,
        _state: &mut Self::State,
        event: &iced::Event,
        bounds: Rectangle,
        cursor: Cursor,
    ) -> Option<iced::widget::Action<Message>>   {

        let mut rectangles: Vec<Rectangle> = Vec::new();
        rectangles.push(Rectangle{x: 210.0, y: 755.0, width: 65.0, height: 10.0});
        rectangles.push(Rectangle{x: 350.0, y: 755.0, width: 65.0, height: 10.0});
        rectangles.push(Rectangle{x: 490.0, y: 755.0, width: 65.0, height: 10.0});
        rectangles.push(Rectangle{x: 630.0, y: 755.0, width: 65.0, height: 10.0});

        match (event, cursor.position_in(bounds)) {

            (canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), cursor_position) => {

                let clicked_rect = rectangles.iter().position(|rect| rect.contains(cursor_position.expect("no point")));
                match clicked_rect {
                    Some(0) => {
                        return Some(iced::widget::Action::publish(Message::DateRange(0)))                         
                    },
                    Some(1) => {
                        return Some(iced::widget::Action::publish(Message::DateRange(1)))
                    },
                    Some(2) => {
                        return Some(iced::widget::Action::publish(Message::DateRange(2)))
                    },
                    Some(3) => {
                        return Some(iced::widget::Action::publish(Message::DateRange(3)))
                    },
                    _ => {println!("nothing")}
                }
                return None
            }
            _ => return None
        } // end of match statement
    } // end update function
}

when a message is delivered to the app's update() routine, the framework will always call the view() function afterwards, and a redraw is always triggered.

iced does NOT store a copy of "prevous" app state, thus it cannot "compare" if the state is actually changed or not after a message is delivered, the message updates the app state in-place instead.

note, unlike many traditional gui frameworks, iced imposes no limit on your app state type[1], which makes iced so flexible, but it also means it cannot do "smart diff" again your app state to optimize the redraw.

however, this is not to way, you cannot do any optimization to the redraw procedure in such architectures (commonly known as the elm architecture, or elm-like architectures). but it is mostly up to the "backend" (the widget tree, individual widgets, the renderer, etc), not the "frontend" (the entrypoint, glue code, app logic, etc).

basically, there are two opportunities for the optimization, you can implement either or both, depending on the nature of the widget and app data:

  • if a low level event (user input, timer, async wakeup, etc) only changes the states local to the widget, simply don't bubble it up to the upper layer (the app or a reusable component), an internal redraw for a single widget may be scheduled, which is much cheaper.

  • cache the previous render result, and only draw the delta of data when a redraw is triggered by the upper layer. this is why widgets can have private states that are persistent across frames.


  1. other than a 'static bound, and optionally a Default bound if you don't have a custom boot() function, i.e. the difference between iced::application() and iced::run() â†Šī¸Ž

Thank you once again for helping me understand how rust works. To control state changes which would in turn cause unnecessary redraws, I modified the canvas program update function and added a local state variable which contains the current bounding rectangles of interest. In the update function I add and subtract bounding rectangles to this state variable accordingly to stop the unnecessary canvas redraws. Below is my local state variable and update function.

#[derive(Clone)]
pub struct MyRect {
   pub id: u32,
   pub bounds: Rectangle,
}

#[derive(Clone)]
pub struct CanvasState {
    pub default_rects: Vec<MyRect>,
    pub rectangles: Vec<MyRect>,
}

impl Default for CanvasState {
    fn default() -> Self {
        let rect1 = vec![
            MyRect {id: 0, bounds: Rectangle{x: 210.0, y: 755.0, width: 65.0, height: 10.0}},
            MyRect {id: 1, bounds: Rectangle{x: 350.0, y: 755.0, width: 65.0, height: 10.0}},
            MyRect {id: 2, bounds: Rectangle{x: 490.0, y: 755.0, width: 65.0, height: 10.0}},
            MyRect {id: 3, bounds: Rectangle{x: 630.0, y: 755.0, width: 65.0, height: 10.0}}
        ];
        let rect2 = vec![
            MyRect {id: 1, bounds: Rectangle{x: 350.0, y: 755.0, width: 65.0, height: 10.0}},
            MyRect {id: 2, bounds: Rectangle{x: 490.0, y: 755.0, width: 65.0, height: 10.0}},
            MyRect {id: 3, bounds: Rectangle{x: 630.0, y: 755.0, width: 65.0, height: 10.0}}
        ];
        CanvasState { default_rects: rect1, rectangles: rect2 }
    }
}

    fn update(
        &self,
        state: &mut Self::State,
        event: &iced::Event,
        bounds: Rectangle,
        cursor: Cursor,
        ) -> Option<iced::widget::Action<Message>>   {

        match (event, cursor.position_in(bounds)) {

            (canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), cursor_position) => {

                let clicked_rect = state.rectangles
                    .iter()
                    .find(|rect| rect.bounds.contains(cursor_position.expect("not in a rectangle")));
                    
                match clicked_rect {
                    Some(rect) if rect.id == 0 => {
                        state.rectangles = state.default_rects.clone();
                        state.rectangles.retain(|rect| rect.id != 0);
                        return Some(iced::widget::Action::publish(Message::DateRange(0)))                         
                    },
                    Some(rect) if rect.id == 1 => {
                        state.rectangles = state.default_rects.clone();
                        state.rectangles.retain(|rect| rect.id != 1);
                        return Some(iced::widget::Action::publish(Message::DateRange(1)))
                    },
                    Some(rect) if rect.id == 2 => {
                        state.rectangles = state.default_rects.clone();
                        state.rectangles.retain(|rect| rect.id != 2);
                        return Some(iced::widget::Action::publish(Message::DateRange(2)))
                    },
                    Some(rect) if rect.id == 3 => {
                        state.rectangles = state.default_rects.clone();
                        state.rectangles.retain(|rect| rect.id != 3);
                        return Some(iced::widget::Action::publish(Message::DateRange(3)))
                    },
                    _ => {println!("state not updated")}
            }
            return None
            }
            _ => return None
        } // end of match statement
    } // end update function