1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-05-25 11:50:17 +02:00

Replace GUI exercise with Logger (#1682)

This should be a bit simpler, and notably
* does not require trait objects, which per #1516 should be moved later
in the course
 * does not require a lot of futzing with string formatting

But all that hard work developing the GUI exercise is not for naught: it
remains in the "Modules" segment, where students will get a chance to
read some Rust code and reorganize it a little bit.

Fixes #1617.

R=mgeisler as the original author of the GUI exercise.
This commit is contained in:
Dustin J. Mitchell 2024-01-18 14:15:19 -05:00 committed by GitHub
parent b4164e44a3
commit 9d9b4170e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 202 additions and 178 deletions

4
Cargo.lock generated
View File

@ -1355,6 +1355,10 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "modules"
version = "0.1.0"
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.11" version = "0.2.11"

View File

@ -13,6 +13,7 @@ members = [
"src/std-types", "src/std-types",
"src/std-traits", "src/std-traits",
"src/iterators", "src/iterators",
"src/modules",
"src/testing", "src/testing",
"src/memory-management", "src/memory-management",
"src/smart-pointers", "src/smart-pointers",

View File

@ -78,7 +78,7 @@
- [Traits](methods-and-traits/traits.md) - [Traits](methods-and-traits/traits.md)
- [Deriving](methods-and-traits/deriving.md) - [Deriving](methods-and-traits/deriving.md)
- [Trait Objects](methods-and-traits/trait-objects.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) - [Solution](methods-and-traits/solution.md)
- [Generics](generics.md) - [Generics](generics.md)
- [Generic Functions](generics/generic-functions.md) - [Generic Functions](generics/generic-functions.md)
@ -167,7 +167,7 @@
- [Filesystem Hierarchy](modules/filesystem.md) - [Filesystem Hierarchy](modules/filesystem.md)
- [Visibility](modules/visibility.md) - [Visibility](modules/visibility.md)
- [`use`, `super`, `self`](modules/paths.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) - [Solution](modules/solution.md)
- [Testing](testing.md) - [Testing](testing.md)
- [Test Modules](testing/unit-tests.md) - [Test Modules](testing/unit-tests.md)

View File

@ -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 Let's design a simple logging utility, using a trait `Logger` with a `log`
trait objects. We'll only implement the drawing of it (as text) for simplicity. 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. This is a common pattern: a struct wrapping a trait implementation and
- `Button`: has a `label`. In reality, it would also take a callback function to implementing that same trait, adding behavior in the process. What other kinds
allow the program to do something when the button is clicked but we won't of wrappers might be useful in a logging utility?
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 <https://play.rust-lang.org/>, fill in the missing
`draw_into` methods so that you implement the `Widget` trait:
```rust,compile_fail ```rust,compile_fail
// TODO: remove this when you're done with your implementation.
#![allow(unused_imports, unused_variables, dead_code)]
{{#include exercise.rs:setup}} {{#include exercise.rs:setup}}
// TODO: Implement `Widget` for `Label`. // TODO: Define and implement `VerbosityFilter`.
// TODO: Implement `Widget` for `Button`.
// TODO: Implement `Widget` for `Window`.
{{#include exercise.rs:main}} {{#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");
println!("centered: |{:/^width$}|", "foo");
println!("right 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! | |
| +-----------+ |
+--------------------------------+
```

View File

@ -1,4 +1,4 @@
// Copyright 2022 Google LLC // Copyright 2024 Google LLC
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -14,124 +14,44 @@
// ANCHOR: solution // ANCHOR: solution
// ANCHOR: setup // ANCHOR: setup
pub trait Widget { use std::fmt::Display;
/// Natural width of `self`.
fn width(&self) -> usize;
/// Draw the widget into a buffer. pub trait Logger {
fn draw_into(&self, buffer: &mut dyn std::fmt::Write); /// Log a message at the given verbosity level.
fn log(&self, verbosity: u8, message: impl Display);
}
/// Draw the widget on standard output. struct StderrLogger;
fn draw(&self) {
let mut buffer = String::new(); impl Logger for StderrLogger {
self.draw_into(&mut buffer); fn log(&self, verbosity: u8, message: impl Display) {
println!("{buffer}"); eprintln!("verbosity={verbosity}: {message}");
} }
} }
pub struct Label { fn do_things(logger: &impl Logger) {
label: String, logger.log(5, "FYI");
} logger.log(2, "Uhoh");
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),
)
}
} }
// ANCHOR_END: setup // ANCHOR_END: setup
impl Widget for Window { /// Only log messages up to the given verbosity level.
fn width(&self) -> usize { struct VerbosityFilter<L: Logger> {
// Add 4 paddings for borders max_verbosity: u8,
self.inner_width() + 4 inner: L,
}
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, "+-{:-<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 { impl<L: Logger> Logger for VerbosityFilter<L> {
fn width(&self) -> usize { fn log(&self, verbosity: u8, message: impl Display) {
self.label.width() + 8 // add a bit of padding if verbosity <= self.max_verbosity {
} self.inner.log(verbosity, message);
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();
} }
} }
// ANCHOR: main // ANCHOR: main
fn main() { fn main() {
let mut window = Window::new("Rust GUI Demo 1.23"); let l = VerbosityFilter { max_verbosity: 3, inner: StderrLogger };
window.add_widget(Box::new(Label::new("This is a small text GUI demo."))); do_things(&l);
window.add_widget(Box::new(Button::new("Click me!")));
window.draw();
} }
// ANCHOR_END: main // ANCHOR_END: main

9
src/modules/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "modules"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "modules"
path = "exercise.rs"

View File

@ -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 In this exercise, you will reorganize a small GUI Library implementation. This
and Traits" segment of the course into a collection of modules. It is typical to library defines a `Widget` trait and a few implementations of that trait, as
put each type or set of closely-related types into its own module, so each well as a `main` function.
widget type should get its own module.
If you no longer have your version, that's fine - refer back to the It is typical to put each type or set of closely-related types into its own
[provided solution](../methods-and-traits/solution.html). module, so each widget type should get its own module.
## Cargo Setup ## Cargo Setup
@ -23,8 +22,16 @@ cd gui-modules
cargo run cargo run
``` ```
Edit `src/main.rs` to add `mod` statements, and add additional files in the Edit the resulting `src/main.rs` to add `mod` statements, and add additional
`src` directory. files in the `src` directory.
## Source
Here's the single-module implementation of the GUI library:
```rust
{{#include exercise.rs:single-module}}
```
<details> <details>

132
src/modules/exercise.rs Normal file
View File

@ -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<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 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, "+-{:-<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();
}