练习:面向 GUI 库的模块

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

Rust Playground 仅支持一个文件,因此您需要在本地文件系统上创建一个 Cargo 项目:

  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.

鼓励学生按照自己认为合适的方式划分代码,并熟悉必需的 modusepub 声明。之后,讨论哪些组织方式最符合惯例。