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:

  1. cargo init gui-modules
  2. cd gui-modules
  3. cargo 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:

  1. pub trait Widget {
  2.     /// Natural width of `self`.
  3.     fn width(&self) -> usize;
  4.     /// Draw the widget into a buffer.
  5.     fn draw_into(&self, buffer: &mut dyn std::fmt::Write);
  6.     /// Draw the widget on standard output.
  7.     fn draw(&self) {
  8.         let mut buffer = String::new();
  9.         self.draw_into(&mut buffer);
  10.         println!("{buffer}");
  11.     }
  12. }
  13. pub struct Label {
  14.     label: String,
  15. }
  16. impl Label {
  17.     fn new(label: &str) -> Label {
  18.         Label { label: label.to_owned() }
  19.     }
  20. }
  21. pub struct Button {
  22.     label: Label,
  23. }
  24. impl Button {
  25.     fn new(label: &str) -> Button {
  26.         Button { label: Label::new(label) }
  27.     }
  28. }
  29. pub struct Window {
  30.     title: String,
  31.     widgets: Vec<Box<dyn Widget>>,
  32. }
  33. impl Window {
  34.     fn new(title: &str) -> Window {
  35.         Window { title: title.to_owned(), widgets: Vec::new() }
  36.     }
  37.     fn add_widget(&mut self, widget: Box<dyn Widget>) {
  38.         self.widgets.push(widget);
  39.     }
  40.     fn inner_width(&self) -> usize {
  41.         std::cmp::max(
  42.             self.title.chars().count(),
  43.             self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),
  44.         )
  45.     }
  46. }
  47. impl Widget for Window {
  48.     fn width(&self) -> usize {
  49.         // Add 4 paddings for borders
  50.         self.inner_width() + 4
  51.     }
  52.     fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
  53.         let mut inner = String::new();
  54.         for widget in &self.widgets {
  55.             widget.draw_into(&mut inner);
  56.         }
  57.         let inner_width = self.inner_width();
  58.         // TODO: Change draw_into to return Result<(), std::fmt::Error>. Then use the
  59.         // ?-operator here instead of .unwrap().
  60.         writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
  61.         writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();
  62.         writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();
  63.         for line in inner.lines() {
  64.             writeln!(buffer, "| {:inner_width$} |", line).unwrap();
  65.         }
  66.         writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();
  67.     }
  68. }
  69. impl Widget for Button {
  70.     fn width(&self) -> usize {
  71.         self.label.width() + 8 // add a bit of padding
  72.     }
  73.     fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
  74.         let width = self.width();
  75.         let mut label = String::new();
  76.         self.label.draw_into(&mut label);
  77.         writeln!(buffer, "+{:-<width$}+", "").unwrap();
  78.         for line in label.lines() {
  79.             writeln!(buffer, "|{:^width$}|", &line).unwrap();
  80.         }
  81.         writeln!(buffer, "+{:-<width$}+", "").unwrap();
  82.     }
  83. }
  84. impl Widget for Label {
  85.     fn width(&self) -> usize {
  86.         self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)
  87.     }
  88.     fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {
  89.         writeln!(buffer, "{}", &self.label).unwrap();
  90.     }
  91. }
  92. fn main() {
  93.     let mut window = Window::new("Rust GUI Demo 1.23");
  94.     window.add_widget(Box::new(Label::new("This is a small text GUI demo.")));
  95.     window.add_widget(Box::new(Button::new("Click me!")));
  96.     window.draw();
  97. }

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.