Exercise: Modules for a GUI Library
In this exercise, you will reorganize a small GUI Library implementation. This library defines a Widget trait and a few implementations of that trait, as well as a main function.
It is typical to put each type or set of closely-related types into its own module, so each widget type should get its own module.
Cargo Setup
The Rust playground only supports one file, so you will need to make a Cargo project on your local filesystem:
cargo init gui-modulescd gui-modulescargo run
Edit the resulting src/main.rs to add mod statements, and add additional files in the src directory.
Source
Here’s the single-module implementation of the GUI library:
pub trait Widget {/// Natural width of `self`.fn width(&self) -> usize;/// Draw the widget into a buffer.fn draw_into(&self, buffer: &mut dyn std::fmt::Write);/// Draw the widget on standard output.fn draw(&self) {let mut buffer = String::new();self.draw_into(&mut buffer);println!("{buffer}");}}pub struct Label {label: String,}impl Label {fn new(label: &str) -> Label {Label { label: label.to_owned() }}}pub struct Button {label: Label,}impl Button {fn new(label: &str) -> Button {Button { label: Label::new(label) }}}pub struct Window {title: String,widgets: Vec<Box<dyn Widget>>,}impl Window {fn new(title: &str) -> Window {Window { title: title.to_owned(), widgets: Vec::new() }}fn add_widget(&mut self, widget: Box<dyn Widget>) {self.widgets.push(widget);}fn inner_width(&self) -> usize {std::cmp::max(self.title.chars().count(),self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),)}}impl Widget for Window {fn width(&self) -> usize {// Add 4 paddings for bordersself.inner_width() + 4}fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {let mut inner = String::new();for widget in &self.widgets {widget.draw_into(&mut inner);}let inner_width = self.inner_width();// TODO: Change draw_into to return Result<(), std::fmt::Error>. Then use the// ?-operator here instead of .unwrap().writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();for line in inner.lines() {writeln!(buffer, "| {:inner_width$} |", line).unwrap();}writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();}}impl Widget for Button {fn width(&self) -> usize {self.label.width() + 8 // add a bit of padding}fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {let width = self.width();let mut label = String::new();self.label.draw_into(&mut label);writeln!(buffer, "+{:-<width$}+", "").unwrap();for line in label.lines() {writeln!(buffer, "|{:^width$}|", &line).unwrap();}writeln!(buffer, "+{:-<width$}+", "").unwrap();}}impl Widget for Label {fn width(&self) -> usize {self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)}fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {writeln!(buffer, "{}", &self.label).unwrap();}}fn main() {let mut window = Window::new("Rust GUI Demo 1.23");window.add_widget(Box::new(Label::new("This is a small text GUI demo.")));window.add_widget(Box::new(Button::new("Click me!")));window.draw();}
This slide and its sub-slides should take about 15 minutes.
Encourage students to divide the code in a way that feels natural for them, and get accustomed to the required mod, use, and pub declarations. Afterward, discuss what organizations are most idiomatic.