diff --git a/Cargo.lock b/Cargo.lock index d80d64cb..4acd9b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1355,6 +1355,10 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "modules" +version = "0.1.0" + [[package]] name = "native-tls" version = "0.2.11" diff --git a/Cargo.toml b/Cargo.toml index 600c3481..ec55bef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "src/std-types", "src/std-traits", "src/iterators", + "src/modules", "src/testing", "src/memory-management", "src/smart-pointers", diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 758b685d..11b19264 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -78,7 +78,7 @@ - [Traits](methods-and-traits/traits.md) - [Deriving](methods-and-traits/deriving.md) - [Trait Objects](methods-and-traits/trait-objects.md) - - [Exercise: GUI Library](methods-and-traits/exercise.md) + - [Exercise: Generic Logger](methods-and-traits/exercise.md) - [Solution](methods-and-traits/solution.md) - [Generics](generics.md) - [Generic Functions](generics/generic-functions.md) @@ -167,7 +167,7 @@ - [Filesystem Hierarchy](modules/filesystem.md) - [Visibility](modules/visibility.md) - [`use`, `super`, `self`](modules/paths.md) - - [Exercise: Modules for the GUI Library](modules/exercise.md) + - [Exercise: Modules for a GUI Library](modules/exercise.md) - [Solution](modules/solution.md) - [Testing](testing.md) - [Test Modules](testing/unit-tests.md) diff --git a/src/methods-and-traits/exercise.md b/src/methods-and-traits/exercise.md index d20f189f..c11faad6 100644 --- a/src/methods-and-traits/exercise.md +++ b/src/methods-and-traits/exercise.md @@ -1,75 +1,26 @@ --- -minutes: 30 +minutes: 20 --- -# Exercise: GUI Library +# Exercise: Generic Logger -Let us design a classical GUI library using our new knowledge of traits and -trait objects. We'll only implement the drawing of it (as text) for simplicity. +Let's design a simple logging utility, using a trait `Logger` with a `log` +method. Code which might log its progress can then take an `&impl Logger`. In +testing, this might put messages in the test logfile, while in a production +build it would send messages to a log server. -We will have a number of widgets in our library: +However, the `StderrLogger` given below logs all messages, regardless of +verbosity. Your task is to write a `VerbosityFilter` type that will ignore +messages above a maximum verbosity. -- `Window`: has a `title` and contains other widgets. -- `Button`: has a `label`. In reality, it would also take a callback function to - allow the program to do something when the button is clicked but we won't - include that since we're only drawing the GUI. -- `Label`: has a `label`. - -The widgets will implement a `Widget` trait, see below. - -Copy the code below to , fill in the missing -`draw_into` methods so that you implement the `Widget` trait: +This is a common pattern: a struct wrapping a trait implementation and +implementing that same trait, adding behavior in the process. What other kinds +of wrappers might be useful in a logging utility? ```rust,compile_fail -// TODO: remove this when you're done with your implementation. -#![allow(unused_imports, unused_variables, dead_code)] - {{#include exercise.rs:setup}} -// TODO: Implement `Widget` for `Label`. - -// TODO: Implement `Widget` for `Button`. - -// TODO: Implement `Widget` for `Window`. +// TODO: Define and implement `VerbosityFilter`. {{#include exercise.rs:main}} ``` - -The output of the above program can be something simple like this: - -```text -======== -Rust GUI Demo 1.23 -======== - -This is a small text GUI demo. - -| Click me! | -``` - -If you want to draw aligned text, you can use the -[fill/alignment](https://doc.rust-lang.org/std/fmt/index.html#fillalignment) -formatting operators. In particular, notice how you can pad with different -characters (here a `'/'`) and how you can control alignment: - -```rust,editable -fn main() { - let width = 10; - println!("left aligned: |{:/width$}|", "foo"); -} -``` - -Using such alignment tricks, you can for example produce output like this: - -```text -+--------------------------------+ -| Rust GUI Demo 1.23 | -+================================+ -| This is a small text GUI demo. | -| +-----------+ | -| | Click me! | | -| +-----------+ | -+--------------------------------+ -``` diff --git a/src/methods-and-traits/exercise.rs b/src/methods-and-traits/exercise.rs index 78dbe277..f432c98b 100644 --- a/src/methods-and-traits/exercise.rs +++ b/src/methods-and-traits/exercise.rs @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,124 +14,44 @@ // ANCHOR: solution // ANCHOR: setup -pub trait Widget { - /// Natural width of `self`. - fn width(&self) -> usize; +use std::fmt::Display; - /// Draw the widget into a buffer. - fn draw_into(&self, buffer: &mut dyn std::fmt::Write); +pub trait Logger { + /// Log a message at the given verbosity level. + fn log(&self, verbosity: u8, message: impl Display); +} - /// Draw the widget on standard output. - fn draw(&self) { - let mut buffer = String::new(); - self.draw_into(&mut buffer); - println!("{buffer}"); +struct StderrLogger; + +impl Logger for StderrLogger { + fn log(&self, verbosity: u8, message: impl Display) { + eprintln!("verbosity={verbosity}: {message}"); } } -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>, -} - -impl Window { - fn new(title: &str) -> Window { - Window { title: title.to_owned(), widgets: Vec::new() } - } - - fn add_widget(&mut self, widget: Box) { - 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), - ) - } +fn do_things(logger: &impl Logger) { + logger.log(5, "FYI"); + logger.log(2, "Uhoh"); } // ANCHOR_END: setup -impl Widget for Window { - fn width(&self) -> usize { - // Add 4 paddings for borders - self.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: after learning about error handling, you can change - // draw_into to return Result<(), std::fmt::Error>. Then use - // the ?-operator here instead of .unwrap(). - writeln!(buffer, "+-{:- { + max_verbosity: u8, + inner: L, } -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, "+{:- Logger for VerbosityFilter { + fn log(&self, verbosity: u8, message: impl Display) { + if verbosity <= self.max_verbosity { + self.inner.log(verbosity, message); } - writeln!(buffer, "+{:- 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(); } } // ANCHOR: main 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(); + let l = VerbosityFilter { max_verbosity: 3, inner: StderrLogger }; + do_things(&l); } // ANCHOR_END: main diff --git a/src/modules/Cargo.toml b/src/modules/Cargo.toml new file mode 100644 index 00000000..a86b0386 --- /dev/null +++ b/src/modules/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "modules" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "modules" +path = "exercise.rs" diff --git a/src/modules/exercise.md b/src/modules/exercise.md index 82d0bc3a..5a5a7b5f 100644 --- a/src/modules/exercise.md +++ b/src/modules/exercise.md @@ -1,16 +1,15 @@ --- -minutes: 20 +minutes: 15 --- -# Exercise: Modules for the GUI Library +# Exercise: Modules for a GUI Library -In this exercise, you will reorganize the GUI Library exercise from the "Methods -and Traits" segment of the course into a collection of modules. 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. +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. -If you no longer have your version, that's fine - refer back to the -[provided solution](../methods-and-traits/solution.html). +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 @@ -23,8 +22,16 @@ cd gui-modules cargo run ``` -Edit `src/main.rs` to add `mod` statements, and add additional files in the -`src` directory. +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: + +```rust +{{#include exercise.rs:single-module}} +```
diff --git a/src/modules/exercise.rs b/src/modules/exercise.rs new file mode 100644 index 00000000..09fbb19f --- /dev/null +++ b/src/modules/exercise.rs @@ -0,0 +1,132 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ANCHOR: single-module +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>, +} + +impl Window { + fn new(title: &str) -> Window { + Window { title: title.to_owned(), widgets: Vec::new() } + } + + fn add_widget(&mut self, widget: Box) { + 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 borders + self.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, "+-{:- 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, "+{:- 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(); +}