DEV Community

Dev TNG
Dev TNG

Posted on

Building a Desktop Time App in Rust with GPUI: A Beginner's Journey

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 Render traits for UI components
  • Type Safety: Compile-time guarantees that prevent runtime timezone errors
  • Lifetimes: Understanding &mut Context and window references

GPUI Framework Fundamentals

  • Component architecture with impl IntoElement
  • State management via Entity and Context
  • 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
Enter fullscreen mode Exit fullscreen mode

Project Setup

  1. Create new project:
cargo new world-time-app
cd world-time-app
Enter fullscreen mode Exit fullscreen mode
  1. Add dependencies to Cargo.toml:
[dependencies]
chrono = { version = "0.4" }
gpui = "0.2"
gpui-component = "0.4.0-preview1"
Enter fullscreen mode Exit fullscreen mode
  1. Copy the code to src/main.rs

  2. Build and run:

cargo run --release
Enter fullscreen mode Exit fullscreen mode

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 --release only 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
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Debugging Strategies That Saved Me

1. Print Debugging Still Works

dbg!(&self.time); // Rust's debug macro
println!("City: {}, Time: {}", self.name, self.time);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

4. Read the Compiler Suggestions

Rust's compiler often suggests fixes:

help: consider borrowing here
  |
5 |     render(&time);
  |            +
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

  1. The Rust Book (official) - Read chapters 1-10 first
  2. Rust by Example - Hands-on code snippets
  3. Rustlings - Interactive exercises that compile when correct
  4. 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 time to 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)