Iced + Canvas + Text: How do I create a style for canvas text

I have a program that graphs data onto an iced canvas. To clean up the canvas draw function I moved the code into a series of helper functions. In several of these functions I deal with displaying canvas text and to do so I apply values to all of the text structure elements. To further clean up the code, I would like to be able to create a style (I think that's the correct term) for the canvas text so that I can call out the content, location, and size elements then defer to a style for the balance. Below are 2 code snippets that I hope will be helpful. Any assistance will be appreciated.

impl<Message> canvas::Program<Message> for LineGraph {

    type State = ();

    fn draw(
        &self,
        _state: &(),
        renderer: &Renderer,
        _theme: &Theme,
        bounds: iced::Rectangle,
        _cursor: iced::advanced::mouse::Cursor,
    ) -> Vec<canvas::Geometry<Renderer>> {

    let mut frame = canvas::Frame::new(renderer, bounds.size());
    
    draw_heading(&mut frame);
    draw_axis(&mut frame);
    draw_grid(&mut frame);
    draw_x_axis_ticks(&mut frame);
    draw_x_axis_labels(&mut frame, &self.x_values);
    draw_y_axis_ticks(&mut frame);
    draw_y_axis_labels(&mut frame, &self.y_values);
    draw_data_points(&mut frame, &self.points);

    vec![frame.into_geometry()]
        
    } // end of function draw

}

pub fn draw_x_axis_labels(frame: &mut Frame, x_labels: &Vec<String>) {

    for (index, label) in x_labels.iter().enumerate() {

        frame.fill_text(Text {
            content: label.to_string(),
            position: Point { x: 100.0 + (index as f32 * 120.0), y: 718.0 }, 
            max_width: 60.0,
            color: Color::from_rgb(0.0, 0.0, 0.0),
            size: iced::Pixels(12.0),
            line_height: iced::advanced::text::LineHeight::Relative(1.2),
            font: iced::Font::default(),
            align_x: iced::advanced::text::Alignment::Center,
            align_y: iced::alignment::Vertical::Center,
            shaping: iced::advanced::text::Shaping::Auto,
        });
    }
}


if you want rich text, you need to store the styling attributes along with the text, the plain Strings are not enough.

for example, suppose you need to control the size and color of each label, you can do something like:

struct StyledLabel {
	text: String,
	size: Pixels,
	color: Color,
	//... other attributes
}

struct LineGraph {
	//...
	x_values: Vec<StyledLabel>,
	y_values: Vec<StyledLabel>,
}

pub fn draw_x_axis_labels(frame: &mut Frame, x_labels: &[StyledLabel]) {
    for (i, label) in x_labels.iter().cloned().enumerate() {
        frame.fill_text(Text {
            content: label.text,
            color: label.color,
            size: label.size,
            //... populate other attributes as before
        })
    }
}

Thank you for the response. In my original helper function I used the code below (less the commented out code). Unfortunately the default does not give me what I want, so I called out all elements as indicated in my original post. Is there a way to modify the defaults so I can use the code below and get what I want or use something like ..Text::MyDefaults() where MyDefaults contains what I want?

pub fn draw_x_axis_labels(frame: &mut Frame, x_labels: &Vec<String>) {
    for (index, label) in x_labels.iter().enumerate() {
        frame.fill_text(Text {
            content: label.to_string(),
            position: Point { x: 100.0 + (index as f32 * 120.0), y: 718.0 }, 
            ..Text::default()

            // max_width: 80.0,
            // color: Color::from_rgb(0.0, 0.0, 0.0),
            // size: iced::Pixels(12.0),
            // line_height: iced::advanced::text::LineHeight::Relative(1.2),
            // font: iced::Font::default(),
            // align_x: iced::advanced::text::Alignment::Center,
            // align_y: iced::alignment::Vertical::Center,
            // shaping: iced::advanced::text::Shaping::Auto,
        });
    }
}

yes, there is.

this is called "functional update syntax" in the reference, or "struct update syntax" in the book.

the .. token is followed by a value of the same struct type, it doesn't need to be Default::default(), or it doesn't have to be a function call, you can put any value there, it's just Default::default() is a very common idiom because it's a standard library trait with well defined semantics.

struct Foo {
    x: i32,
    y: usize,
}

// the "default" value, I call it fallback here
fn fallback() -> Foo { Foo { x: 123, y: 456 } }

let foo1 = Foo {
    x: -123,
    ..fallback(),
};

// or you can use a local variable
let default = fallback();
let foo2 = Foo {
    y: 789,
    ..default
};

// or a named constant if the type is `const`-constructible
const DEFAULT: Foo = Foo { x: 123, y: 456 };
let foo2 = Foo {
    y: 789,
    ..DEFAULT
};

Absolutely fantastic. I had no idea this could be done. It is appreciated that you went the extra mile to include the link to the book. Thank you. Below is my updated code.

impl<Message> canvas::Program<Message> for LineGraph {

    type State = ();

    fn draw(
        &self,
        _state: &(),
        renderer: &Renderer,
        _theme: &Theme,
        bounds: iced::Rectangle,
        _cursor: iced::advanced::mouse::Cursor,
    ) -> Vec<canvas::Geometry<Renderer>> {

    let text_defaults = Text {
            content: "bad label".to_string(),
            position: Point { x: 500.0, y: 500.0 }, 
            max_width: 80.0,
            color: Color::from_rgb(0.0, 0.0, 0.0),
            size: iced::Pixels(12.0),
            line_height: iced::advanced::text::LineHeight::Relative(1.2),
            font: iced::Font::default(),
            align_x: iced::advanced::text::Alignment::Center,
            align_y: iced::alignment::Vertical::Center,
            shaping: iced::advanced::text::Shaping::Auto,
    };
    let mut frame = canvas::Frame::new(renderer, bounds.size());
   
    draw_heading(&mut frame);
    draw_axis(&mut frame);
    draw_grid(&mut frame);
    draw_x_axis_ticks(&mut frame);
    draw_x_axis_labels(&mut frame, &self.x_values, text_defaults);
    draw_y_axis_ticks(&mut frame);
    draw_y_axis_labels(&mut frame, &self.y_values);
    draw_data_points(&mut frame, &self.points);

    vec![frame.into_geometry()]
        
    } // end of function draw
}

pub fn draw_x_axis_labels(frame: &mut Frame, x_labels: &Vec<String>, defaults: Text) {
    for (index, label) in x_labels.iter().enumerate() {
        frame.fill_text(Text {
            content: label.to_string(),
            position: Point { x: 100.0 + (index as f32 * 140.0), y: 718.0 }, 
            ..defaults
        });
    }
}

you are constructing the Text in a loop, you need to use ..defaults.clone(), since the struct is not Copy.

but personally I would not pass the default value as an argument. I would propably refactor the code into a function like this:

fn label_text_at(content: String, position: Point) -> Text {
    Text {
        content,
        position,
        // the defaults
        max_width: .80.0,
        //...
    }
}

pub fn draw_x_axis_labels(frame: &mut Frame, x_labels: &[String]) {
    for (index, label) in x_labels.iter().cloned().enumerate() {
        let position = Point { x: 100.0 + (index as f32 * 140.0), y: 718.0 };
        frame.fill_text(label_text_at(label, position));
    }
}

I implemented your suggestion. It is better but have a question. The x_labels vector passed into the function is only used to create labels on the canvas, which I should have indicated. Is it better to use cloned and make copies of the vectors data or to use into_iter which I think takes ownership of the data? Both work, with the only difference that I can see is that cloned returns label as a String and into_iter returns label as a &String which I convert with .to_string().
Below is the code:

pub fn text_label(content: String, position:  Point) -> Text {
    Text {
        content: content,
        position: position,
        max_width: 80.0,
        color: Color::from_rgb(0.0, 0.0, 0.0),
        size: iced::Pixels(12.0),
        line_height: iced::advanced::text::LineHeight::Relative(1.2),
        font: iced::Font::default(),
        align_x: iced::advanced::text::Alignment::Center,
        align_y: iced::alignment::Vertical::Center,
        shaping: iced::advanced::text::Shaping::Auto,
    }
}

pub fn draw_x_axis_labels(frame: &mut Frame, x_labels: &Vec<String>) {
    for (index, label) in x_labels.into_iter().enumerate() {
            let position = Point { x: 100.0 + (index as f32 * 140.0), y: 718.0 };
            frame.fill_text(text_label(label.to_string(), position));         
    }
}

into_iter() only makes a difference when the argument was passed by value, i.e. Vec<String> instead of &Vec<String> (or &[String]). when the argument is a reference, into_iter() actually calls the implementation of IntoIterator::into_iter() for &Vec, it's actually exactly the same as Vec::iter(): the Item yielded by the iterator is always &String, not String.

in fact, if you only need to iterate through the elements of a &Vec, you can drop either .iter() or into_iter() completely, because IntoIterator is what the for loop syntax sugar actually use. in this case however, since you need to call .enumerate() (and .cloned() too in my example), you need to manually convert &Vec to an Iterator, thus .iter() or .into_iter() is needed, but both are equivalent.

yes, both works. in fact, both are the same. choose whichever you like. it's just personal habit.

my example use Iterator::cloned(), your code uses to_string(), but the effect is the same: the ToString::to_string() implementation for String is specialized and is equivalent to clone().

it would be more efficient if you could move the Vec into the function, a.k.a. take the argument by value, but it is impossible in this case: the signature of canvas::Program::draw() takes &self, not self, not even &mut self, so you cannot move the data self.x_values out of the LineGraph. you have to clone the String for drawing.

and there's a good reason for it: widgets (the canvas, in this case) might need to redraw multiple times without triggering update to the app states. in other words, the view states created by the view() function can be used to draw multiple frames for the same widget tree, it cannot be "consumed" by a single frame.

another reason the cloning is necessary is in the drawing API, that is, Frame::fill_text() takes an owned String as argument (indirectly with impl Into<Text>). to my understanding, this is to support deferred renderers, the ones which could take advantage of batched (or lazy) rendering pipelines.

so there's always a trade-off: eager text shaping (and even rasterization) can eliminate the need to allocate the String, but it's (very likely) going to hurt the rendering performance, plus, even if you shape the text eagerly, you still need to store the glyphs anyway. over all, I think it is good trace-off.