Building a Desktop Time App in Rust with GPUI: A Beginner's Journey
Source: Dev.to
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 the 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
Entitythat 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 a 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.rs. - Build and run:
cargo run --release
Common Build Issues & Solutions
-
Issue: “cannot find
gpuiin 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: The 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. Choosing 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) -> 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 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)
// 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
- Print Debugging Still Works
dbg!(&self.time); // Rust's debug macro
println!("City: {}, Time: {}", self.name, self.time);
Lessons Learned
1. Keep the Boilerplate Minimal
fn main() {
// Minimal setup
let app = App::new();
app.run();
}
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?
- 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. 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?
| Symbol | Meaning |
|---|---|
& | Read‑only borrow (most common) |
&mut | Exclusive write access |
| (none) | Transfer ownership (consuming the value) |
Start with &, add mut when you need to modify, and transfer ownership only when necessary.
Performance Insights from This Project
Memory Usage
- Compiled binary: ~4 MB (release mode with optimizations)
- Runtime memory: ~8 MB for 5 timezone cards
- No garbage collector overhead
Startup Time
- Cold start: ~200 ms on M1 Mac
- Hot start: ~50 ms
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)
| Activity | Hours |
|---|---|
| Reading docs / tutorials | 40 |
| Fighting compiler errors | 60 |
| Rewriting after understanding | 15 |
| Actual feature coding | 10 |
| “Why isn’t the window showing?” | 8 |
| Total | 133 |
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