Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace GUI exercise with Logger #1682

Merged
merged 4 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ members = [
"src/std-types",
"src/std-traits",
"src/iterators",
"src/modules",
"src/testing",
"src/memory-management",
"src/smart-pointers",
Expand Down
4 changes: 2 additions & 2 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
75 changes: 13 additions & 62 deletions src/methods-and-traits/exercise.md
Original file line number Diff line number Diff line change
@@ -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 <https://play.rust-lang.org/>, 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");
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! | |
| +-----------+ |
+--------------------------------+
```
124 changes: 22 additions & 102 deletions src/methods-and-traits/exercise.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);

/// 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,
pub trait Logger {
/// Log a message at the given verbosity level.
fn log(&self, verbosity: u8, message: impl Display);
}

impl Label {
fn new(label: &str) -> Label {
Label { label: label.to_owned() }
}
}

pub struct Button {
label: Label,
}
struct StderrLogger;

impl Button {
fn new(label: &str) -> Button {
Button { label: Label::new(label) }
impl Logger for StderrLogger {
fn log(&self, verbosity: u8, message: impl Display) {
eprintln!("verbosity={verbosity}: {message}");
}
}

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),
)
}
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, "+-{:-<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();
}
/// Only log messages up to the given verbosity level.
struct VerbosityFilter<L: Logger> {
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, "+{:-<width$}+", "").unwrap();
for line in label.lines() {
writeln!(buffer, "|{:^width$}|", &line).unwrap();
impl<L: Logger> Logger for VerbosityFilter<L> {
fn log(&self, verbosity: u8, message: impl Display) {
if verbosity <= self.max_verbosity {
self.inner.log(verbosity, message);
}
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
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
9 changes: 9 additions & 0 deletions src/modules/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "modules"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "modules"
path = "exercise.rs"
27 changes: 17 additions & 10 deletions src/modules/exercise.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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}}
```

<details>

Expand Down
Loading