Learn Rust Through the Photo Framer App — Enums, Pattern Matching & Option/Result
Key concept: Rust enums are algebraic data types — each variant can carry its own data. Paired with match, they replace null checks…
Learn Rust Through the Photo Framer App — Enums, Pattern Matching & Option/Result
Key concept: Rust enums are algebraic data types — each variant can carry its own data. Paired with match, they replace null checks, exception handling, and a lot of polymorphism-by-class-hierarchy in one stroke.
Previous: Ownership, Borrowing & Lifetimes

generated based on the context of the article
Enums in Photo Framer
Simple Enum
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BorderMode {
Percentage,
Pixels,
}
This looks like a C/C++ enum or a TypeScript string union, but Rust enums are more powerful — each variant can hold data. Here, BorderMode is just a tag.
The derive gives us:
Clone + Copy— the value is bit-copyable (no heap data, just a tag).PartialEq— we can compare with==, which is whatselectable_valuein egui needs.Debug— we get{:?}printing for free.
Enum with Data
Here’s where Rust gets interesting. Our Border type actually does carry data — either a percentage or a pixel count:
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Border {
Percent(f32),
Pixels(u32),
}
Each variant holds a different payload. When we write Border::Percent(5.0), we’re saying "five percent of the shortest side." When we write Border::Pixels(20), we’re saying "exactly twenty pixels." We can never accidentally mix them up — they're different values of the same type.
This is the Rust equivalent of a TypeScript discriminated union:
type Border =
| { kind: "percent"; value: number }
| { kind: "pixels"; value: number };
But in Rust, the compiler guarantees exhaustive handling. If we ever add a Border::Auto variant, every match on Border becomes a compile error until we handle it. That's a refactoring safety net we lean on hard.
Our Error Type Is Also an Enum
#[derive(Debug)]
pub enum FrameError {
Open { path: PathBuf, source: image::ImageError },
Save { path: PathBuf, source: image::ImageError },
Io { path: PathBuf, source: std::io::Error },
Encode(image::ImageError),
}
Three struct-like variants and one tuple variant, all in one type. This is covered in depth in the following article, but the point I want to make here is that enums scale — they’re not just “a better C enum”, they’re the primary tool for expressing “this value is one of a fixed set of shapes.”
Pattern Matching with match
Basic Match
Here’s how Border resolves itself to pixels:
impl Border {
fn resolve(self, min_side: u32) -> u32 {
match self {
Border::Percent(p) => ((min_side as f32) * p / 100.0).round().max(1.0) as u32,
Border::Pixels(px) => px.max(1),
}
}
}
match is a switch that:
- Must be exhaustive — every variant must be handled, or I use
_as a catch-all. - Is an expression — the whole thing returns a value.
- Destructures — the
pinPercent(p)binds the inner float for us.
Match on a String Slice
pub fn detect_format(path: &Path) -> Option<ImageFormat> {
let ext = path.extension().and_then(|e| e.to_str())?.to_ascii_lowercase();
match ext.as_str() {
"jpg" | "jpeg" => Some(ImageFormat::Jpeg),
"png" => Some(ImageFormat::Png),
"tif" | "tiff" => Some(ImageFormat::Tiff),
"bmp" => Some(ImageFormat::Bmp),
"webp" => Some(ImageFormat::WebP),
_ => None,
}
}
The | inside a pattern is an "or" — "jpg" | "jpeg" matches either string. Much cleaner than a chain of if guards.
Notice the ? on the first line: .and_then(|e| e.to_str()) gives us Option<&str>. If that's None (no extension or non-UTF-8 bytes), the whole function early-returns None. Then we can work with a plain &str inside the match.
The matches! Macro
A macro in Rust is a metaprogramming construct that generates code at compile time. matches!(value, pattern) is a convenience macro that returns a bool. We use it when we only care about "does this match?" and don't need to bind any inner data. The | inside the pattern works the same as in a regular match.
Option — Rust’s Replacement for Null
Rust has no null. A value that might not exist is wrapped in Option<T>:
enum Option<T> {
Some(T),
None,
}
It lives in the prelude, so we never import it —we just write Some(x) and None directly.
Option in Photo Framer
path.extension().and_then(|e| e.to_str())
Two chained fallible steps:
extension()→Option<&OsStr>(the path might not have an extension at all).and_then(|e| e.to_str())→Option<&str>(theOsStrmight not be valid UTF-8)
If any step returns None, the whole chain short-circuits to None. It's like optional chaining in TypeScript (path.extension?.toString()), but the types are explicit the whole way through.
Common Option Methods

we picked up .map_or_else while cleaning up the file list panel:
let display = path.file_name().map_or_else(
|| path.to_string_lossy().to_string(),
|n| n.to_string_lossy().to_string(),
);
Reads as: “if there’s a file name, use it; otherwise fall back to the whole path.” One expression, no temporaries.
if let — Conditional Destructuring
if let Some(rx) = &self.done_rx {
if rx.try_recv().is_ok() {
self.processing = false;
self.done_rx = None;
}
}
if let is the Rust idiom for "do something if the value is present." It's cleaner than a full match when we only care about one variant.
Result<T, E> — Errors as Values
enum Result<T, E> {
Ok(T),
Err(E),
}
Rust has no exceptions. All fallible operations return Result. I cover error handling in depth in [t](06 - Error Handling)he following article, but the connection to enums is the key point: Result is just a two-variant enum, and we handle it with the same match / if let / method chains I use for Option.
Exercises
- Add a new variant
Border::Autothat picks a percentage for large images and a pixel count for small ones. Watch how the compiler marches you through everymatchonBorderuntil you've handled it. - Rewrite
detect_formatusingif letchains instead ofmatchonext.as_str(). Which do you think reads better? - Write a function
fn parse_dimension(s: &str) -> Option<Border>that parses strings like"5%"or"20px"into aBorder. Practice combiningOptionmethods instead of reaching formatch.
메타데이터
- post_id
- 0249299d79db
- slug
- learn-rust-through-the-photo-framer-app-enums-pattern-matching-option-result-0249299d79db
- url
- https://medium.com/@DPeriel/learn-rust-through-the-photo-framer-app-enums-pattern-matching-option-result-0249299d79db
- canonical_url
- https://medium.com/@DPeriel/learn-rust-through-the-photo-framer-app-enums-pattern-matching-option-result-0249299d79db
- author_url
- https://medium.com/@DPeriel
- status
- ok
- fetched_at
- 2026-06-12 18:14:10