Learning Rust through real projects reveals the language's steep but rewarding learning curve. This world clock desktop app took six weeks to build using GPUI (Zed's UI framework), demonstrating common beginner challenges: lifetime management, trait bounds, and the borrow checker—but ultimately proving that persistence pays off with memory-safe, performant applications.
The Rust Time Paradox: A Developer's Story
I set out to build a simple world clock app in Rust. Six weeks later, I finally got it working—displaying current time in five cities. The irony? I spent so much time making a time display that time itself seemed to stop.
What This Project Teaches You
Core Rust Concepts You'll Master
-
Ownership & Borrowing: Every timezone card is an
Entity<WorldTime>that manages its own state -
Trait Implementation: Custom
Rendertraits for UI components - Type Safety: Compile-time guarantees that prevent runtime timezone errors
-
Lifetimes: Understanding
&mut Contextand window references
GPUI Framework Fundamentals
- Component architecture with
impl IntoElement - State management via
EntityandContext - Reactive updates using
cx.observe_window_bounds() - Layout system with flexbox-style APIs
How to Build and Run This Project
Prerequisites
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --version
Project Setup
- Create new project:
cargo new world-time-app
cd world-time-app
-
Add dependencies to
Cargo.toml:
[dependencies]
chrono = { version = "0.4" }
gpui = "0.2"
gpui-component = "0.4.0-preview1"
Copy the code to
src/main.rsBuild and run:
cargo run --release
Common Build Issues & Solutions
Issue: "cannot find gpui in the registry"
- Solution: GPUI is only available via git, not crates.io. Use the git dependency as shown above.
Issue: Compilation takes 10+ minutes
-
Solution: First build is slow (downloading/compiling dependencies). Subsequent builds use cargo's cache. Use
--releaseonly for final builds.
Issue: Window doesn't appear on Linux
- Solution: Install required system libraries:
# Ubuntu/Debian
sudo apt-get install libxkbcommon-dev libwayland-dev
# Fedora
sudo dnf install libxkbcommon-devel wayland-devel
Code Architecture Breakdown
The WorldTime Entity
pub struct WorldTime {
name: String,
time: String, // HH:MM format
diff_hours: i32, // offset from home
is_home: bool,
timezone_id: String,
}
Key learning: Structs in Rust must own their data or explicitly borrow it. Using String (owned) vs &str (borrowed) was my first major decision.
Component Rendering Pattern
impl Render for WorldTime {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement
}
Key learning: The &mut self parameter confused me for days. Mutable references allow the component to update its state during render cycles.
Time Updates
if now.duration_since(self.last_update).as_secs() >= 60 {
for city in &self.cities {
city.update(cx, |city, _cx| {
city.update_time();
});
}
}
Key learning: The update() method on Entity<T> takes a context and a closure. The closure receives &mut T (mutable access to the entity's internal state) and a context, allowing safe state mutations within GPUI's ownership model.
For Rust Beginners: What I Wish I Knew
Week 1-2: The Compiler is Your Teacher
Challenge: "Why won't this compile?!"
// ❌ This fails
let time = WorldTime::new(...);
render(time);
use_time_again(time); // ERROR: value moved
// ✅ This works
let time = WorldTime::new(...);
render(&time);
use_time_again(&time); // Borrowing, not moving
Lesson: Rust's error messages are detailed. Read them carefully—they often include the fix.
Week 3-4: Lifetimes Aren't Scary
Challenge: Understanding 'a in trait bounds
Breakthrough moment: Lifetimes just tell the compiler "this reference is valid for this long." The compiler infers most of them.
// GPUI handles this complexity:
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>)
// You don't write the lifetimes manually
Week 5-6: Fighting the Borrow Checker
The infamous error:
error[E0502]: cannot borrow `self.cities` as mutable because it is also borrowed as immutable
My solution: Extract logic into separate functions. The borrow checker works per-scope:
// Instead of doing everything in one method:
fn render(&mut self) {
let cities = &self.cities; // Immutable borrow
self.update_all(); // ❌ Mutable borrow while immutable exists
}
// Split into logical units:
fn render(&mut self) {
self.update_if_needed(); // Mutable borrow ends here
let cities = &self.cities; // Now we can borrow immutably
}
Debugging Strategies That Saved Me
1. Print Debugging Still Works
dbg!(&self.time); // Rust's debug macro
println!("City: {}, Time: {}", self.name, self.time);
2. Start Simple, Add Complexity
My first version just showed one city. Then two. Then five. Each step compiled before moving forward.
3. Use cargo check Constantly
cargo check # Fast syntax checking without building
cargo clippy # Linting suggestions
cargo fmt # Auto-formatting
4. Read the Compiler Suggestions
Rust's compiler often suggests fixes:
help: consider borrowing here
|
5 | render(&time);
| +
FAQ: Common Rust Learner Questions
Why is Rust so hard to learn?
Rust forces you to think about memory management upfront rather than debugging crashes later. The learning curve is steep but prevents entire classes of bugs (use-after-free, data races, null pointer dereferences).
How long until I'm productive in Rust?
In my experience: 2-3 weeks for basic syntax, 2-3 months for comfortable development. This project (6 weeks) taught me more than any tutorial because I had to solve real problems.
Should I learn C++ first?
No. Rust's ownership system is unique. C++ habits can actually make Rust harder. Start with Rust directly.
What if my code doesn't compile?
Normal for beginners: I spent 70% of my time fighting compiler errors. This is the Rust learning process. Each error teaches you something.
When should I use .clone()?
When you need multiple owned copies. Cloning has performance costs but unblocks development. Optimize later:
// Quick solution:
let city_copy = city.clone();
// Optimized later:
let city_ref = &city;
How do I know when to use &, &mut, or no reference?
-
&= read-only borrow (most common) -
&mut= exclusive write access - No reference = transfer ownership (consuming the value)
Start with &, add mut when you need to modify, transfer ownership rarely.
Performance Insights from This Project
Memory Usage
- Compiled binary: ~4MB (release mode with optimizations)
- Runtime memory: ~8MB for 5 timezone cards
- No garbage collector overhead
Startup Time
- Cold start: ~200ms on M1 Mac
- Hot start: ~50ms
Update Efficiency
The 60-second update check runs every render frame but only recalculates when needed—demonstrating Rust's zero-cost abstractions.
Resources That Actually Helped
Essential Learning Path
- The Rust Book (official) - Read chapters 1-10 first
- Rust by Example - Hands-on code snippets
- Rustlings - Interactive exercises that compile when correct
- This project - Real-world application of concepts
When You're Stuck
- Rust Users Forum: Friendly community for beginners
- r/rust subreddit: Daily questions thread
- Rust Discord: Real-time help
- Compiler errors: Seriously, read them twice
GPUI-Specific Resources
- Zed GitHub repo - Source code examples
- GPUI examples in
/crates/gpui/examples/ - Zed's own codebase - production GPUI usage
The Unvarnished Truth: My Time Investment
Hour Breakdown (Estimated)
- Reading docs/tutorials: 40 hours
- Fighting compiler errors: 60 hours
- Rewriting after understanding: 15 hours
- Actual feature coding: 10 hours
- "Why isn't the window showing?": 8 hours
Total: ~133 hours over 6 weeks
Was It Worth It?
Yes. The second Rust project took me 3 days. The third took hours. The compiler errors that took hours in week 1 now take minutes.
Rust's difficulty is front-loaded. You pay the learning cost once and reap benefits forever: no segfaults, no data races, no surprise runtime panics.
Next Steps: Extending This Project
Beginner-Friendly Additions
- Add more cities (just copy the pattern)
- Change colors/styling (modify RGB values)
- Add date display (extend
timeto include date)
Intermediate Challenges
- Read cities from a config file (learn file I/O)
- Add/remove cities dynamically (learn UI state management)
- Show analog clocks (learn drawing APIs)
Advanced Features
- System tray integration
- Daylight saving time handling
- Multiple time zones per city
- Network time sync
The Real Lesson: Embrace the Struggle
If your Rust code compiles on the first try, you're not learning—you're copying. The borrow checker errors, the lifetime annotations you don't understand, the hours debugging "why won't this move?"—that's the learning process.
My time app works flawlessly now. No memory leaks. No race conditions. No undefined behavior. The compiler guaranteed that before the first run.
The time I "lost" wasn't lost—it was invested in understanding a language that respects your time in production.
Project Repository
Want to try building this yourself? The complete code is available in the document above. Start with a simple version (one city), get it compiling, then add complexity incrementally.
Remember: Every Rust developer has spent hours on compiler errors. The difference between beginners and experts is that experts have made more mistakes.
Built with Rust 1.75+, GPUI (Zed framework), determination, and a healthy relationship with the borrow checker.
"The compiler is mean to you so your users never have to be." - Rust Community Wisdom
Top comments (0)