diff --git a/docs/desktop_applications/index.md b/docs/desktop_applications/index.md index b382a27..8205ffe 100644 --- a/docs/desktop_applications/index.md +++ b/docs/desktop_applications/index.md @@ -1,3 +1,150 @@ -# Intorduction +# Introduction +## Rust Desktop Applications in Slint +Hey there! Super excited to welcome you to the world of desktop application development. We know it might seem a bit daunting at first, but trust us, you're in for a really cool ride. + +We're going to dive deep into Rust – a programming language that's making waves because it helps you build incredibly fast and reliable apps, all while keeping things safe. Then, we'll jump into Slint, a UI toolkit that lets you design awesome-looking desktop interfaces with ease, perfectly paired with Rust's power. + +Our goal here isn't just to throw a bunch of code at you. We want to show you how much fun it can be to actually _build_ something tangible that runs right on your computer. By the end of this course, we genuinely hope you'll be hooked on Rust and ready to start tackling your own real-world projects. Let's get building some amazing stuff together! + +# Course Content: + +## Day 1 (11 August) + +### Lesson 1: Rust Fundamentals - Core Language & Environment Setup + +Objective: Establish a foundational understanding of the Rust programming language, its unique value proposition, and essential development environment setup. + +Topics: + +- The Strategic Purpose of the Rust Language (5 min) +- Rust's Paradigm Shift: Safety and Performance (5 min) +- Strategic Advantages of Rust for Desktop Application Development (5 min) +- Setting Up the Rust Development Environment (15 min) +- Understanding Cargo: Rust's Build System and Package Manager Ecosystem (5-10 min) +- Initiating and Executing Your First Rust Application (5-10 min) +- Variables and Data Declaration in Rust (20 min) +- Console Interaction: Input/Output Stream Management (20 min) + +### Lesson 2: Rust Fundamentals - Control Flow & Functions + +Objective: Master Rust's core control flow constructs and the principles of function declaration for logical program execution. + +Topics: + +- Conditional Execution: if Statements (15 min) +- Advanced Pattern Matching: The match Expression (10 min) +- Iterative Control Structures (30 min) +- Defining and Utilizing Functions (15 min) +- Practical Application: Developing a Console +- Based Guessing Game (20 min) + +## Day 2 (12 August) + +### Lesson 3: Rust Fundamentals - Advanced Concepts & Data Structures + +Objective: Delve into Rust's unique memory management model and advanced data structuring capabilities. + +Topics: + +- Demystifying Ownership, Borrowing, and Lifecycles (30 min) +- Structuring Data with structs (30 min) +- Modeling Data with enums (10 min) +- Organizing Code with Modules (20 min) + +### Lesson 4: Introduction to Slint - Building Static User Interfaces + +Objective: Introduce the Slint UI framework, its integration with Rust, and the creation of basic static graphical interfaces. + +Topics: + +- Introducing Slint: A Declarative UI Framework (5 min) +- Synergies: Why Slint is an Ideal Partner for Rust (5 min) +- Setting Up Your First Slint Application (20 min) +- Understanding the .slint Language and Basic UI Syntax (15 min) +- Core Slint UI Elements (20 min) +- Structuring Your UI: Essential Layout Managers (15 min) +- Developing Your First Static Desktop Application (10 min) + +## Day 3 (13 August) + +### Lesson 5: Slint Interactivity - State, Events, and Dynamic UIs + +Objective: Enable dynamic user interfaces by implementing state management, data binding, and event handling within Slint applications. + +Topics: + +- State Management and Data Binding (30 min) +- Handling User Interactions: Events and Callbacks (including Error Handling) (30 min) +- Practical Application: Developing an Interactive Calculator Application (30 min) + +### Lesson 6: Network Communication - Foundations of HTTP & Asynchronous Rust + +Objective: Understand the principles of inter-computer communication via HTTP and master asynchronous programming in Rust for non-blocking network requests. + +Topics: + +- The Imperative of Inter-Computer Communication (5 min) +- HTTP Protocol: Structure and Fundamentals (5 min) +- Introduction to Asynchronous Rust (30 min) +- Making External API Calls with reqwest and tokio (25 min) +- Data Serialization/Deserialization: Processing API Responses with serde (25 min) + +## Day 4 (15 August) + +### Lesson 7: API Integration & UI Updates + +Objective: Apply asynchronous API communication skills to build dynamic UI features that consume and display external data. + +Topics: + +- Practical Exercises: Consuming Diverse External APIs and Parsing Responses +- Dynamically Updating Slint UI Elements with Collected Data + +### Lesson 8: Advanced Slint Features - Reusability & Customization + +Objective: Enhance UI development efficiency and flexibility through advanced Slint component design and styling techniques. + +Topics: + +- Understanding the Power of UI Components (5 min) +- Benefits of Component Reusability in UI Development (5 min) +- Crafting Your First Reusable Slint Component (20 min) +- Customizing UI Aesthetics: Creating Bespoke Styles (20 min) +- Practical Application Exercise (40 min) + +## Day 5 (16 August) + +### Lesson 9: Data Persistence - Local Storage & File System + +Objective: Implement strategies for local data storage, enabling applications to retain information across sessions and interact with the file system. + +Topics: + +- The Necessity of Persistent Data in Desktop Applications (5 min) +- Overview of Local Data Persistence Strategies (5 min) +- Direct File System Interactions (20 min) +- Achieving Robust Local Data Persistence: Integrating an Embedded Database (60 min) + +### Lesson 10: Desktop Application Best Practices & Deployment + +Objective: Equip participants with essential knowledge for building production-ready desktop applications, including robust error management, lifecycle considerations, and deployment strategies. + +Topics: + +- Comprehensive Error Handling Strategies in Rust Applications +- Packaging and Deployment Fundamentals for Cross-Platform Desktop Apps +- Key Best Practices for Application Architecture and Lifecycle Management +- Project Brainstorming and Future Learning Pathways +- Project Sprints: Collaborative Development & Presentation + +## Day 6 (18 August) + +Project Development Sprint (Part 1) - Working on the final project +Project Development Sprint (Part 2) - Working on the final project + +## Day 7 (19 August) + +Project Presentation (Part 1) +Project Presentation (Part 2) diff --git a/docs/desktop_applications/lesson1.md b/docs/desktop_applications/lesson1.md new file mode 100644 index 0000000..424326e --- /dev/null +++ b/docs/desktop_applications/lesson1.md @@ -0,0 +1,338 @@ +--- +title: Core Language & Environment Setup +sidebar_position: 1 +--- + +### 1.1 The Strategic Purpose of the Rust Language (5 min) + +**Why was Rust created?** Well, languages like C and C++ are incredibly powerful and fast, but they come with a big responsibility: manual memory management. This often leads to common, hard-to-find bugs like: + + * **Memory Leaks:** Your program uses up more and more memory over time and never releases it. + * **Dangling Pointers:** You try to use memory that's already been freed, leading to crashes. + * **Data Races:** When multiple parts of your program try to access and change the same data at the same time, leading to unpredictable results. + +Rust was born to prevent these kinds of bugs *at compile time* (meaning, before your program even runs\!), giving developers more confidence and making software more robust. + +----- + +### 1.2 Rust's Paradigm Shift: Safety and Performance (5 min) + +So, how does Rust achieve this magic trick of being both fast *and* safe? This is where Rust introduces a "paradigm shift" – a new way of thinking about how code interacts with memory. + + * **Safety without a Garbage Collector:** Languages like Python and JavaScript use a "Garbage Collector" (GC) to automatically clean up memory. This is convenient, but it can sometimes introduce pauses or make performance less predictable. Rust achieves memory safety *without* a GC. Instead, it has a set of strict rules that are checked by something called the **"Borrow Checker"** *before* your code even compiles. If your code breaks a rule, it simply won't compile, forcing you to fix potential bugs early. + * **Performance:** Because there's no garbage collector and you have low-level control, Rust code runs incredibly fast, often comparable to C and C++. + * **Fearless Concurrency:** Building programs that do multiple things at once (concurrency) is notoriously hard and bug-prone. Rust's safety rules extend to concurrency, allowing you to write multi-threaded code with much greater confidence that it won't have data races. + +This combination of performance and guaranteed safety is what makes Rust so unique and powerful. + +----- + +### 1.3 Strategic Advantages of Rust for Desktop Application Development (5 min) + +Why are we learning Rust specifically for desktop applications, especially with Slint? + +1. **Native Performance:** Desktop apps need to be snappy and responsive. Rust compiles directly to machine code, giving you blazing-fast performance that feels native to the operating system. No slow loading times or choppy animations\! +2. **Reliability:** Users expect desktop apps to be stable and not crash. Rust's memory safety guarantees mean fewer runtime errors and crashes due to common programming mistakes. Your apps will simply be more robust. +3. **Cross-Platform Potential:** Rust, combined with UI frameworks like Slint, makes it easier to write your application logic once and compile it for Windows, macOS, and Linux, reaching a wider audience. +4. **Growing Ecosystem:** The Rust community is vibrant, and its ecosystem for UI development (with tools like Slint) is rapidly maturing, offering powerful libraries and a great developer experience. + +----- + +### 1.4 Setting Up the Rust Development Environment (15 min) + +Alright, let's get your computer ready to write some Rust code\! The easiest and official way to install Rust is using `rustup`. + +1. **Open Your Terminal/Command Prompt:** + + * **Linux/macOS:** Open your regular terminal application. + * **Windows:** Open PowerShell or Command Prompt. (If you're using VS Code, its integrated terminal works great\!) + +2. **Install `rustup`:** + + * **Linux/macOS:** Copy and paste this command into your terminal and press Enter. Follow the on-screen prompts (usually just pressing Enter for default options). + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + * **Windows:** + * Go to the official `rustup` download page: [https://rustup.rs/](https://rustup.rs/) + * Download the appropriate installer for your system (e.g., `rustup-init.exe` for 64-bit Windows). + * Run the installer and follow the instructions. Choose the default installation options. + +3. **Configure Your Shell (Important for Linux/macOS):** + + * After `rustup` finishes on Linux/macOS, it will often tell you to run a command to add Rust to your system's `PATH`. This is usually: + ```bash + source "$HOME/.cargo/env" + ``` + * Run this command in your *current* terminal session. To make it permanent, you might need to add it to your shell's configuration file (like `.bashrc`, `.zshrc`, or `.profile`). + +4. **Verify Installation:** + + * Close and reopen your terminal/command prompt (or run the `source` command). + * Type these commands to check if Rust and Cargo (Rust's build tool, which comes with `rustup`) are installed correctly: + ```bash + rustc --version + cargo --version + ``` + * You should see version numbers printed for both `rustc` (the Rust compiler) and `cargo`. If you do, congratulations, Rust is installed\! + +5. **Install VS Code (Recommended IDE):** + + * If you don't have it already, download and install Visual Studio Code: [https://code.visualstudio.com/](https://code.visualstudio.com/) + * **Install the `Rust Analyzer` Extension:** This is crucial for a great Rust development experience in VS Code (code completion, error checking, formatting, etc.). Open VS Code, go to the Extensions view (Ctrl+Shift+X or Cmd+Shift+X), search for "Rust Analyzer", and install it. + +----- + +### 1.5 Understanding Cargo: Rust's Build System and Package Manager Ecosystem (5-10 min) + +You just installed `cargo` along with `rustup`. Cargo is like your best friend in Rust development. It's much more than just a package manager; it's Rust's **build system and package manager** rolled into one. + +**What is a Package Manager?** +Think of `npm` for JavaScript, `pip` for Python, or `Maven`/`Gradle` for Java. A package manager helps you: + + * Download and install external libraries (called "crates" in Rust). + * Manage your project's dependencies (what other libraries your code needs). + * Ensure everyone working on a project uses the same versions of libraries. + +**Why is Cargo so useful?** +Cargo streamlines almost every aspect of your Rust workflow: + + * **Project Creation:** `cargo new` quickly sets up a new Rust project with the correct structure. + * **Building:** `cargo build` compiles your code into an executable program. + * **Running:** `cargo run` builds and then executes your program. + * **Testing:** `cargo test` runs your project's tests. + * **Dependency Management:** You declare your project's dependencies in a file called `Cargo.toml`, and Cargo handles downloading and linking them. + * **Publishing:** `cargo publish` helps you share your own Rust libraries (crates) with the world on [crates.io](https://crates.io/). + +Cargo is a central part of the Rust ecosystem and makes development much smoother. + +----- + +### 1.6 Initiating and Executing Your First Rust Application (5-10 min) + +Let's create our classic "Hello, world\!" program using Cargo. + +1. **Create a New Project:** + + * In your terminal, navigate to a directory where you want to create your project (e.g., your Desktop or a `dev` folder). + * Run this command: + ```bash + cargo new hello_rust_app + ``` + * Cargo will create a new folder named `hello_rust_app` with a basic project structure inside. + +2. **Explore the Project Structure:** + + * Navigate into the new folder: `cd hello_rust_app` + * Look at the contents: + * `Cargo.toml`: This is the manifest file for your project. It contains metadata about your project (name, version) and lists its dependencies. + * `src/main.rs`: This is where your main Rust code lives. + * `target/`: (Created after you build) This is where compiled executable files go. + +3. **Examine `src/main.rs`:** + + * Open `src/main.rs` in your VS Code. You'll see: + ```rust + fn main() { + println!("Hello, world!"); + } + ``` + * `fn main()`: This is the main function, the entry point of every Rust executable program. + * `println!`: This is a **macro** (indicated by the `!`). It prints text to the console. + +4. **Run Your Application:** + + * In your terminal (make sure you're inside the `hello_rust_app` folder), run: + ```bash + cargo run + ``` + * **What happens?** + * Cargo first **compiles** your code (you'll see messages like "Compiling hello\_rust\_app v0.1.0..."). + * Then, it **executes** the compiled program. + * You should see: `Hello, world!` printed in your terminal. + +Congratulations\! You've just created and run your very first Rust application. This is a huge milestone\! + +----- + +### 1.7 Variables and Data Declaration in Rust (20 min) + +Now let's dive into how Rust handles variables and data. This is where you'll see some key differences from languages like JavaScript or Python. + +#### **Immutability by Default:** + +This is one of Rust's core principles. By default, variables in Rust are **immutable**, meaning once you give them a value, you cannot change that value. This helps prevent unexpected bugs. + + * **Declaring an Immutable Variable:** + + ```rust + fn main() { + let x = 5; // 'x' is immutable. Its value is 5, and it cannot be changed. + println!("The value of x is: {}", x); + + // x = 6; // This would cause a compile-time error! Try uncommenting it. + // println!("The value of x is: {}", x); + } + ``` + + * **Making a Variable Mutable:** + If you *do* want to change a variable's value, you must explicitly mark it as `mut` (short for mutable). + + ```rust + fn main() { + let mut y = 10; // 'y' is mutable. We can change its value. + println!("The initial value of y is: {}", y); + + y = 15; // This is allowed because 'y' is mutable. + println!("The new value of y is: {}", y); + } + ``` + +#### **Type Inference vs. Explicit Types:** + +Rust is a **statically typed** language, meaning it knows the type of every variable at compile time. However, it's also very smart and can often **infer** the type based on the value you assign. You don't always *have* to write the type. + + * **Type Inference (Common):** + + ```rust + fn main() { + let age = 30; // Rust infers 'age' is an integer (i32 by default) + let pi = 3.14; // Rust infers 'pi' is a floating-point number (f64 by default) + let is_active = true; // Rust infers 'is_active' is a boolean + let initial = 'A'; // Rust infers 'initial' is a character (single quotes) + let greeting = "Hello"; // Rust infers 'greeting' is a string slice (&str) + + println!("Age: {}, Pi: {}, Active: {}, Initial: {}, Greeting: {}", age, pi, is_active, initial, greeting); + } + ``` + + * **Explicit Type Annotation (When needed or for clarity):** + You can explicitly tell Rust the type of a variable. This is useful when inference is ambiguous or for better readability. + + ```rust + fn main() { + let count: i64 = 100_000_000_000; // Explicitly a 64-bit integer + let temperature: f32 = 25.5; // Explicitly a 32-bit float + let message: &str = "Welcome!"; // Explicitly a string slice + + println!("Count: {}, Temp: {}, Message: {}", count, temperature, message); + } + ``` + +#### **Common Primitive Data Types:** + +Rust has several built-in primitive types: + + * **Integers:** `i8`, `i16`, `i32` (default), `i64`, `i128` (signed integers) and `u8`, `u16`, `u32`, `u64`, `u128` (unsigned integers). The number indicates the bits they use. `isize` and `usize` depend on the architecture (e.g., 32-bit or 64-bit). + * **Floating-Point Numbers:** `f32` (single-precision), `f64` (double-precision, default). + * **Booleans:** `bool` (`true` or `false`). + * **Characters:** `char` (single Unicode scalar value, uses single quotes, e.g., `'A'`, `'😊'`). + * **Strings:** We'll learn more about strings later, but for now, know that `&str` (string slice, immutable reference to text) and `String` (growable, owned string) are the main types. + +#### **Constants:** + +Constants are always immutable and must have their type explicitly annotated. They can be declared in any scope, including global. + +```rust +const MAX_POINTS: u32 = 100_000; // Constants are typically named in SCREAMING_SNAKE_CASE +const APP_VERSION: &str = "1.0.0"; + +fn main() { + println!("Max points: {}", MAX_POINTS); + println!("App version: {}", APP_VERSION); +} +``` + +#### **Shadowing:** + +Rust allows you to declare a *new* variable with the same name as a previous variable. This "shadows" the previous variable, meaning the new variable takes precedence. This is different from `mut`, as you're creating a new variable, not changing an existing one. + +```rust +fn main() { + let spaces = " "; // First 'spaces' variable (string slice) + println!("Spaces (initial): '{}'", spaces); + + let spaces = spaces.len(); // 'spaces' is now a new variable, holding the length (an integer) + println!("Spaces (length): {}", spaces); // The old 'spaces' is no longer accessible +} +``` + +Shadowing is useful when you want to transform a variable's value but keep the same name, without needing to make the original variable mutable. + +----- + +### 1.8 Console Interaction: Input/Output Stream Management (20 min) + +Now, let's make our programs a bit more interactive by learning how to print messages to the console and read input from the user. + +#### **Printing to Console (`println!`)** + +You've already seen `println!`. It's a macro for printing text to the standard output (your console), followed by a newline. + + * **Basic Usage:** + + ```rust + fn main() { + println!("Hello, Rustaceans!"); + println!("This is a new line."); + } + ``` + + * **Placeholders for Variables:** You can embed variable values using curly braces `{}`. + + ```rust + fn main() { + let name = "Alice"; + let age = 30; + println!("My name is {} and I am {} years old.", name, age); + println!("The value of 10 + 5 is {}", 10 + 5); + } + ``` + +#### **Reading from Console (`std::io::stdin().read_line()`)** + +Reading user input is a bit more involved in Rust because I/O operations can fail (e.g., the user closes the input stream). Rust forces you to handle these potential failures. + +1. **Import `std::io`:** You need to bring the `io` (input/output) module into scope. + +2. **Get Standard Input:** Use `std::io::stdin()`. + +3. **Read a Line:** Use `.read_line(&mut variable)`. This method takes a *mutable reference* to a `String` where it will store the input. + +4. **Handle the `Result`:** `read_line` returns a `Result` type, which is an `enum` (we'll learn more about enums later\!) that represents either `Ok(value)` for success or `Err(error)` for failure. + + * For now, we'll use `.expect("message")`. This is a quick way to handle `Result`: if it's `Ok`, it gives you the value; if it's `Err`, it crashes your program and prints the message. **This is generally NOT for production code**, but it's simple for learning basic I/O. + +**Example: Asking for User's Name** + +```rust +// main.rs +use std::io; // Bring the standard I/O library into scope + +fn main() { + println!("Hello there! What's your name?"); + + let mut name = String::new(); // Declare a new, empty, mutable String + + // Read a line from standard input and store it in 'name'. + // .expect() will crash the program if reading fails, printing the message. + io::stdin() + .read_line(&mut name) // Pass a mutable reference to 'name' + .expect("Failed to read line"); // Error message if reading fails + + // Input from read_line includes the newline character, so we trim it. + let name = name.trim(); // Shadow 'name' with a new, trimmed string slice + + println!("Nice to meet you, {}!", name); + println!("Your name has {} characters.", name.chars().count()); // Count characters +} +``` + +**Explanation of `String::new()` and `&mut name`:** + + * `String::new()`: This creates a new, empty, growable string. Unlike `&str` (string slices that are fixed-size and often come from literal text), `String` can be modified and owned. + * `&mut name`: This is a **mutable reference** to the `name` variable. `read_line` needs to modify the `String` you pass it, so it requires a mutable reference. This is a glimpse into Rust's borrowing rules, which we'll cover more deeply later. + +----- + +**End of Lesson 1.** You've successfully set up your Rust environment, understood its core philosophy, and written your first interactive console program. Great job\! Take some time to experiment with variables and console I/O. \ No newline at end of file diff --git a/docs/desktop_applications/lesson10.md b/docs/desktop_applications/lesson10.md new file mode 100644 index 0000000..a903a9b --- /dev/null +++ b/docs/desktop_applications/lesson10.md @@ -0,0 +1,292 @@ +--- +title: Best Practices & Deployment +sidebar_position: 10 +--- + +--- + +# Lesson 10: Desktop Application Best Practices & Deployment + +You've learned the fundamentals of Rust, built interactive UIs with Slint, connected to external APIs, and even managed local data. Today, we'll shift our focus to making your applications truly robust and ready to share with the world. We'll dive into best practices for handling errors gracefully and the essential steps to package and deploy your awesome creations. + +--- + +### 10.1 Comprehensive Error Handling Strategies in Rust Applications (30 min) + +In the real world, things go wrong. Network requests fail, files aren't found, users provide invalid input. A good application doesn't just crash; it handles these situations gracefully. Rust's type system, especially `Result` and `Option`, forces us to think about errors, which is a huge advantage\! + +#### **Review: `Result` and `Option`** + +You've already encountered these fundamental enums: + +- **`Option`**: Represents a value that _might_ or _might not_ be present. + + - `Some(T)`: A value is present. + - `None`: No value is present. + - **Use case:** When something _might_ return a value, but it's not an error (e.g., finding an item in a list, parsing a string that might not be a valid number). + +- **`Result`**: Represents an operation that can either succeed or fail. + + - `Ok(T)`: The operation succeeded, returning a value of type `T`. + - `Err(E)`: The operation failed, returning an error of type `E`. + - **Use case:** For operations that can genuinely fail due to external factors (e.g., file I/O, network requests, database operations). + +#### **The `?` Operator: A Shortcut for Error Propagation** + +The `?` operator is syntactic sugar for handling `Result` (and `Option`). It's incredibly common in Rust code. + +- When you use `?` on a `Result`: + - If the `Result` is `Ok(value)`, the `value` is extracted, and the execution continues. + - If the `Result` is `Err(error)`, the `error` is immediately returned from the _current function_, effectively propagating the error up the call stack. +- **Important:** The function using `?` _must_ have a `Result` (or `Option`) as its return type. + + + +```rust +use std::fs::File; +use std::io::{self, Read}; + +// This function now returns a Result, allowing us to use '?' +fn read_file_contents(path: &str) -> Result { + let mut file = File::open(path)?; // '?' will return Err if file opening fails + let mut contents = String::new(); + file.read_to_string(&mut contents)?; // '?' will return Err if reading fails + Ok(contents) // If all is Ok, return the contents +} + +fn main() { + match read_file_contents("non_existent_file.txt") { + Ok(text) => println!("File contents:\n{}", text), + Err(e) => eprintln!("Error reading file: {}", e), // Handle the error + } + + // Example with a successful read (assuming data/config.txt exists from Module 9) + // You might need to create data/config.txt manually or run Module 9's code first + match read_file_contents("data/config.txt") { + Ok(text) => println!("Config file contents:\n{}", text), + Err(e) => eprintln!("Error reading config file: {}", e), + } +} +``` + +#### **Panics vs. Recoverable Errors:** + +- **Panic (`panic!`)**: When a program encounters an **unrecoverable error** (e.g., accessing an out-of-bounds array index, a bug in your code that you can't logically recover from). Panics typically crash the program. You've seen `.unwrap()` and `.expect()` which panic on `Err` or `None`. These are fine for quick examples or when you _know_ a failure indicates a programming bug, but should be avoided in production code for recoverable situations. +- **Recoverable Errors (`Result`)**: For errors that you expect might happen and can handle (e.g., file not found, network timeout). Rust encourages using `Result` for these, allowing your program to react gracefully. + +#### **Advanced Error Handling Crates (Brief Mention):** + +For large applications, managing many different `Error` types can become cumbersome. Crates like `thiserror` and `anyhow` simplify this: + +- **`thiserror`**: Helps you easily define your own custom error `enum`s and automatically implement necessary traits for them. +- **`anyhow`**: Provides a simple, generic `anyhow::Error` type that can wrap almost any other error, making error propagation very easy, especially in `main` functions or top-level logic. + +#### **Error Handling Strategy for Slint UI Applications:** + +1. **Propagate Errors with `?`:** Let errors bubble up from helper functions using `?`. +2. **Handle at the UI Boundary:** When an error reaches your UI logic (e.g., inside an `on_button_clicked` callback), decide how to present it to the user. +3. **Display User-Friendly Messages:** Instead of crashing, update your Slint UI to show a clear, concise message (e.g., "Network unavailable," "File could not be loaded," "Invalid input"). You could use a `Text` element for status messages or a simple pop-up window. +4. **Log Detailed Errors:** Always log the full, technical error information (using a logging crate like `log` or `tracing`) to the console or a log file for debugging purposes. This helps you diagnose issues without overwhelming the user. + + + +```rust +// Conceptual Slint callback with error handling +// (Assuming you have a 'status_text' property in your ui.slint) +// ui.slint: in property status_text; +// ui.slint: Text { text: status_text; color: red; } + +// In src/main.rs: +use log::{error, info}; // Add 'env_logger' to Cargo.toml for easy logging +// In main(): env_logger::init(); // Initialize logger + +// ... +ui.on_load_data_button_clicked(move || { + let ui_handle_clone = ui_handle_weak.clone(); + tokio::spawn(async move { + // This function would return Result<(), Box> + let result = fetch_and_process_data().await; + if let Some(ui) = ui_handle_clone.upgrade() { + match result { + Ok(_) => { + ui.set_status_text("Data loaded successfully!".into()); + info!("Data load operation completed."); + }, + Err(e) => { + // Display user-friendly error in UI + ui.set_status_text(format!("Error: {}", e).into()); + // Log detailed error for debugging + error!("Detailed data load error: {:?}", e); + } + } + } + }); +}); +``` + +--- + +### 10.2 Packaging and Deployment Fundamentals for Cross-Platform Desktop Apps (20 min) + +Once your application is ready, you'll want to share it\! Packaging and deployment is the process of turning your Rust source code into a standalone, runnable application that users can easily install and run on their computers. + +#### **The `cargo build --release` Command:** + +This is your first and most important step. + +- `cargo build`: Compiles your code for development (often with debugging info). +- `cargo build --release`: Compiles your code with **optimizations enabled** and **debugging info stripped**. This results in a much faster and smaller executable file, suitable for distribution. +- **Output:** The compiled executable will be found in `target/release/`. For example, `target/release/my_slint_app.exe` on Windows, `target/release/my_slint_app` on Linux, or `target/release/my_slint_app` (which might need to be bundled into a `.app` on macOS). + +#### **Self-Contained Binaries:** + +One of Rust's great advantages is that its compiled binaries are often **statically linked** by default (or mostly so). This means that most of the Rust standard library and your dependencies are bundled directly into the executable. + +- **Benefit:** Users don't need to install Rust or specific runtime environments (like Node.js runtime or Python interpreter) to run your app. They just need the executable file and any external assets (like images, fonts) you might use. + +#### **Platform-Specific Packaging (High-Level Overview):** + +While `cargo build --release` gives you the executable, users typically expect an installer or a neatly bundled application. + +1. **Windows:** + + - **Installer:** Tools like `cargo-wix` (uses WiX Toolset) can create `.msi` installers. + - **Manual:** You can provide the `.exe` and any necessary `DLL`s (if dynamically linked) and asset folders in a `.zip` file. + - + +2. **macOS:** + + - **`.app` Bundle:** macOS applications are typically distributed as `.app` bundles, which are special directories containing the executable, resources, and metadata. + - You'll often need to create this structure manually or use community tools. + - + +3. **Linux:** + + * **`.deb` (Debian/Ubuntu) / `.rpm` (Fedora/RHEL):** Package managers are common. Tools like `cargo-deb` can help create `.deb` packages. + * **AppImage / Flatpak / Snap:** Universal Linux packaging formats that bundle all dependencies. More complex to set up initially but provide wider compatibility. + * **Manual:** Provide the executable and assets in a `.tar.gz` archive. + * + + **Key Takeaway:** The `cargo build --release` command is your starting point. For a professional distribution, you'll then use platform-specific tools or universal packaging formats to create user-friendly installers or bundles. + +--- + +### 10.3 Key Best Practices for Application Architecture and Lifecycle Management (15 min) + +Building a good application isn't just about making it work; it's about making it maintainable, scalable, and user-friendly. + +#### **Application Architecture Best Practices:** + +1. **Modularity:** Break your code into smaller, focused modules (using Rust's `mod` system) and Slint components. + - **Separation of Concerns:** Keep UI logic (`.slint` and Slint-related Rust code) separate from your core business logic, data models, and API interaction logic. + - **Example:** A `data_models.rs` for structs, an `api_client.rs` for network calls, `db_manager.rs` for database logic, and `ui.slint` for the UI definition. +2. **Clear Data Flow:** Understand how data moves between your Rust logic and your Slint UI (properties, callbacks). Use `Rc` and `Arc` as needed for shared state. +3. **Error Handling:** As discussed, implement robust error handling throughout your application. +4. **Logging:** Use a logging framework (`log` + `env_logger` or `tracing`) to get insights into your application's behavior and diagnose issues. +5. **Testing (Briefly):** Write unit tests for your core Rust logic (functions, structs, algorithms) to ensure correctness. `cargo test` makes this easy. + +#### **Application Lifecycle Management:** + +Desktop applications have a lifecycle that needs to be managed: + +1. **Startup:** + - Initialize logging. + - Load configuration from files. + - Initialize database connections. + - Load initial data (e.g., from an API or local storage). + - Create and run the main Slint window. +2. **Runtime:** + - Handle user input (events). + - Perform background tasks (using `tokio::spawn` for async operations). + - Update the UI based on state changes. +3. **Shutdown:** + - **Graceful Exit:** Handle window close events. + - **Save State:** Save any unsaved data or user preferences to disk (e.g., to your SQLite database or config files). + - **Clean Up:** Close database connections, release resources. + +**Example: Handling Shutdown in Slint (Conceptual):** + +You can often hook into window close events or application exit signals to perform cleanup. + +```rust +// In src/main.rs +// ... +#[tokio::main] +async fn main() -> Result<(), Box> { + // ... setup db connection ... + let ui = MainWindow::new().unwrap(); + let ui_handle_weak = ui.as_weak(); + + // Example: Handle window close event to save data + ui.on_window_close_requested(move || { + // This callback is triggered when the user tries to close the window. + // You can put your save logic here. + if let Some(ui) = ui_handle_weak.upgrade() { + println!("Window close requested. Saving application state..."); + // Call your save_data_to_db(&conn) or save_config_to_file() functions here. + // Be mindful of async operations during shutdown. + // For simplicity, you might just log or perform quick sync saves. + // If you need to perform async tasks, spawn them and ensure they complete. + } + // Return true to allow the window to close, false to prevent it. + true + }); + + ui.run().unwrap(); // This blocks until the window is closed + println!("Application shut down gracefully."); + Ok(()) +} +``` + +--- + +### 10.4 Project Brainstorming and Future Learning Pathways (15 min) + +You've now completed the core curriculum\! You have the tools to build a wide variety of desktop applications. + +#### **Project Brainstorming (For Your Final Project):** + +Think about what you've learned and what excites you. Here are some ideas: + +- **Enhanced Global Insights Dashboard:** + - Add country search/filtering to the list. + - Implement saving favorite countries to local SQLite. + - Include a "review" section for each country (saved to SQLite). + - Fetch and display more complex data (e.g., historical population data, currency exchange rates). +- **Simple Task/To-Do Manager:** + - Add/edit/delete tasks. + - Mark tasks as completed. + - Store tasks in an SQLite database. + - Implement filtering (e.g., show only incomplete tasks). +- **Basic Note-Taking App:** + - Create, view, edit, delete notes. + - Save notes to files or an SQLite database. + - Simple text editor area. +- **Unit Converter:** + - Convert between different units (length, weight, temperature). + - Practice state management and input/output. +- **Simple Game (e.g., Tic-Tac-Toe, Simon Says):** + - Focus on UI interaction, game logic, and state updates. + +#### **Future Learning Pathways:** + +Your journey in Rust and desktop development is just beginning\! + +- **Advanced Rust:** + - Deeper dive into **Traits**, **Generics**, and **Macros**. + - More advanced **Concurrency** patterns (channels, mutexes, atomics). + - **Error Handling** with `thiserror` and `anyhow`. + - **FFI (Foreign Function Interface):** Calling C/C++ libraries from Rust. +- **Advanced Slint:** + - Creating complex **Custom Widgets**. + - Advanced **Animations** and **Transitions**. + - **Theming** and **Styling** in depth. + - **Accessibility** features. + - Using Slint's **Model** system for large datasets. +- **Other Rust UI Frameworks:** Explore alternatives like `egui`, `iced`, `druid`, `tauri` (for web-based UIs). +- **Databases:** Learn more about relational databases (PostgreSQL, MySQL) and NoSQL databases (MongoDB, Redis) and how to connect Rust applications to them. +- **Backend Development:** If you're interested in building web services, explore Rust web frameworks like **Axum**, **Actix-Web**, or **Rocket**. + +--- + +Good luck, and we can't wait to see what amazing applications you build\! diff --git a/docs/desktop_applications/lesson2.md b/docs/desktop_applications/lesson2.md new file mode 100644 index 0000000..1eacb53 --- /dev/null +++ b/docs/desktop_applications/lesson2.md @@ -0,0 +1,321 @@ +--- +title: Control Flow & Functions +sidebar_position: 2 +--- + +----- + +# Lesson 2: Rust Fundamentals - Control Flow & Functions + + We'll learn how to make decisions based on conditions, repeat actions, and organize our code into reusable blocks. By the end, we'll even build a game\! + +----- + +### 2.1 Conditional Execution: `if` Statements (15 min) + +Just like in other programming languages, `if` statements in Rust let your program execute different code blocks based on whether a condition is true or false. + +#### **Basic `if`, `else if`, `else`:** + +You'll find this structure very familiar: + +```rust +fn main() { + let number = 7; + + if number < 5 { // If this condition is true + println!("Condition was true: number is less than 5"); + } else if number == 5 { // Otherwise, if this condition is true + println!("Condition was true: number is exactly 5"); + } else { // If none of the above conditions are true + println!("Condition was false: number is greater than 5"); + } +} +``` + + * **Conditions Must Be `bool`:** In Rust, the condition inside an `if` statement *must* evaluate to a **boolean** (`true` or `false`). You can't just use a number like in some other languages. + ```rust + // This would be an ERROR: `if number` is not allowed in Rust + // if number { + // println!("Number was something!"); + // } + ``` + +#### **`if` as an Expression:** + +A cool feature in Rust is that `if` statements are **expressions**, meaning they can return a value. This is super handy for assigning values conditionally. + +```rust +fn main() { + let condition = true; + let number = if condition { // 'if' expression returns a value + 5 // This value is returned if 'condition' is true + } else { + 6 // This value is returned if 'condition' is false + }; // Note the semicolon here, as it's a statement assigning a value + + println!("The value of number is: {}", number); // Output: The value of number is: 5 + + let message = if number > 5 { + "Number is greater than 5" + } else { + "Number is 5 or less" + }; // Both branches must return the SAME TYPE! + + println!("Message: {}", message); +} +``` + + * **Important:** All branches of an `if` expression **must return the same type**. If one branch returns an integer and another returns a string, Rust won't compile because it can't determine the final type of the variable. + +----- + +### 2.2 Advanced Pattern Matching: The `match` Expression (10 min) + +The `match` expression is one of Rust's most powerful control flow constructs. It allows you to compare a value against a series of patterns and then execute code based on which pattern matches. It's often a more robust and readable alternative to long `if-else if` chains. + + * **Exhaustiveness:** A key feature of `match` is that it must be **exhaustive**. This means you have to cover *every possible value* that the data could take. If you don't, Rust's compiler will give you an error, which helps prevent bugs\! + +Let's look at a simple example with numbers: + +```rust +fn main() { + let number = 3; + + match number { // Match the 'number' against these patterns + 1 => println!("One!"), // If number is 1, do this + 2 => println!("Two!"), // If number is 2, do this + 3 | 4 => println!("Three or Four!"), // If number is 3 OR 4, do this (multiple patterns) + 5..=10 => println!("Between 5 and 10, inclusive!"), // If number is in this range + _ => println!("Something else!"), // The underscore '_' is a catch-all pattern (like 'default' in switch) + } + + let result = match number { // 'match' can also be an expression, returning a value + 1 => "It's one", + _ => "It's not one", // All branches must return the same type! + }; + println!("Result: {}", result); +} +``` + + * **When to use `match` vs. `if`:** + * Use `if` for simple true/false conditions or a few distinct branches. + * Use `match` when you have many possible values or complex patterns to handle, especially when working with `enum`s (which we'll cover in Lesson 3) or `Result` types (which you saw in Lesson 1's I/O). + +----- + +### 2.3 Iterative Control Structures (30 min) + +Repeating actions is a fundamental part of programming. Rust provides several ways to create loops. + +#### **`loop` (Infinite Loop with `break`):** + +The `loop` keyword creates an infinite loop. You'll typically use `break` to exit it based on a condition, and `continue` to skip to the next iteration. + +```rust +fn main() { + let mut counter = 0; + + let result = loop { // 'loop' can also return a value! + counter += 1; + println!("Loop count: {}", counter); + + if counter == 10 { + break counter * 2; // Break the loop and return this value + } + }; // Semicolon here, as it's an expression + + println!("Loop finished. Result: {}", result); // Output: Loop finished. Result: 20 +} +``` + +#### **`while` Loop:** + +A `while` loop executes a block of code repeatedly as long as a specified condition remains true. + +```rust +fn main() { + let mut number = 3; + + while number != 0 { + println!("{}!", number); + number -= 1; // Decrement number + } + println!("LIFTOFF!!!"); +} +``` + +#### **`for` Loop (Iterating over Collections):** + +The `for` loop is the most common loop in Rust. It's used to iterate over elements in a collection (like arrays, vectors, or ranges). This is often safer and more concise than `while` loops for iterating. + + * **Iterating over a Range:** + + ```rust + fn main() { + // Iterate from 1 up to (but not including) 5 + for number in 1..5 { + println!("Number in range: {}", number); + } + // Iterate from 1 up to AND including 5 + for number in 1..=5 { + println!("Number in range (inclusive): {}", number); + } + } + ``` + + * **Iterating over an Array/Vector:** + + ```rust + fn main() { + let a = [10, 20, 30, 40, 50]; + + for element in a.iter() { // .iter() creates an iterator over the elements + println!("The value is: {}", element); + } + + // You can also iterate with an index if needed (less common in idiomatic Rust) + for (index, element) in a.iter().enumerate() { + println!("Element at index {}: {}", index, element); + } + } + ``` + +----- + +### 2.4 Defining and Utilizing Functions (15 min) + +Functions are blocks of code that perform a specific task and can be reused. + +#### **Basic Function Syntax:** + + * Functions are declared using the `fn` keyword. + * Parameters are type-annotated. + * The return type is specified after an arrow `->`. + * The last expression in a function (without a semicolon) is implicitly returned. You can also use the `return` keyword explicitly. + + + +```rust +// A function that doesn't take parameters and doesn't return a value +fn greet() { + println!("Hello from the greet function!"); +} + +// A function that takes parameters and returns a value +fn add_numbers(x: i32, y: i32) -> i32 { // Takes two i32s, returns an i32 + x + y // This is an expression, implicitly returned +} + +// A function with an explicit return +fn subtract_numbers(a: i32, b: i32) -> i32 { + return a - b; // Explicit return +} + +fn main() { + greet(); // Call the greet function + + let sum = add_numbers(5, 7); // Call add_numbers and store the result + println!("The sum is: {}", sum); // Output: The sum is: 12 + + let difference = subtract_numbers(10, 3); + println!("The difference is: {}", difference); // Output: The difference is: 7 +} +``` + +----- + +### 2.5 Practical Application: Developing a Console-Based Guessing Game (20 min) + +Let's put everything we've learned so far into practice by building a simple "Guess the Number" game in the console\! + +**Game Logic:** + +1. Generate a random secret number. +2. Prompt the user to guess. +3. Read the user's input. +4. Compare the guess to the secret number. +5. Tell the user if they guessed too high, too low, or correctly. +6. Keep looping until the user guesses correctly. + +**New Concepts/Tools:** + + * **`rand` crate:** We'll need a library to generate random numbers. Add `rand = "0.8.5"` (or a recent version) to your `Cargo.toml` under `[dependencies]`. + * **`use rand::Rng;`:** To bring the random number generator trait into scope. + * **`parse()` method:** To convert the user's input string to a number. This also returns a `Result`, so we'll handle it. + +**Steps to Build:** + +1. **Create a new Cargo project:** `cargo new guessing_game` +2. **Add `rand` dependency:** Open `Cargo.toml` and add `rand = "0.8.5"` under `[dependencies]`. +3. **Open `src/main.rs`** and replace its content with the following: + + + +```rust +// src/main.rs +use std::io; // For input/output operations +use rand::Rng; // For generating random numbers +use std::cmp::Ordering; // For comparing numbers (used with match) + +fn main() { + println!("Guess the number!"); + + // Generate a random number between 1 and 100 (inclusive) + // thread_rng() gives us a random number generator local to the current thread. + // gen_range(1..=100) generates a number in the specified range. + let secret_number = rand::thread_rng().gen_range(1..=100); + + // For debugging, you can uncomment this line: + // println!("The secret number is: {}", secret_number); + + loop { // Start an infinite loop for the game + println!("Please input your guess:"); + + let mut guess = String::new(); // Create a mutable string to store user input + + // Read the user's guess from the console + io::stdin() + .read_line(&mut guess) + .expect("Failed to read line"); // Handle potential errors + + // Convert the guess from String to a number (u32). + // .trim() removes any whitespace (like the newline character) + // .parse() attempts to convert the string to a number. It returns a Result. + // We use a 'match' expression to handle the Result: + // - If Ok, we get the number. + // - If Err, it means the input wasn't a valid number, so we print an error + // and 'continue' to the next loop iteration (ask for guess again). + let guess: u32 = match guess.trim().parse() { + Ok(num) => num, // If parsing was successful, use the number + Err(_) => { // If parsing failed (e.g., user typed text) + println!("Please type a number!"); + continue; // Skip to the next iteration of the loop + } + }; + + println!("You guessed: {}", guess); + + // Compare the guess to the secret number using 'match' and 'Ordering' enum + // Ordering is an enum with variants Less, Greater, and Equal. + match guess.cmp(&secret_number) { + Ordering::Less => println!("Too small!"), // If guess is less than secret + Ordering::Greater => println!("Too big!"), // If guess is greater than secret + Ordering::Equal => { // If guess is equal to secret + println!("You win!"); + break; // Exit the loop (and the game) + } + } + } +} +``` + +**Run your game:** +In your terminal, navigate into the `guessing_game` folder and run: +`cargo run` + +Now, play the game\! Try typing text instead of numbers to see the error handling. + +----- + +**End of Lesson 2.** You've now mastered Rust's core control flow, functions, and even built a complete interactive game\! This is a huge step in your Rust journey. Next, we'll tackle Rust's most unique and powerful concept: Ownership. \ No newline at end of file diff --git a/docs/desktop_applications/lesson3.md b/docs/desktop_applications/lesson3.md new file mode 100644 index 0000000..2fe4710 --- /dev/null +++ b/docs/desktop_applications/lesson3.md @@ -0,0 +1,499 @@ +--- +title: Advanced Concepts & Data Structures +sidebar_position: 3 +--- + +----- + +# Lesson 3: Rust Fundamentals - Advanced Concepts & Data Structures + +Welcome to the most conceptually challenging, yet incredibly rewarding, part of our Rust journey: understanding **Ownership**. This is what makes Rust unique and powerful. We'll also dive into how to create your own custom data types using `struct`s and `enum`s, and how to organize your growing codebase with modules. + +----- + +### 3.1 Demystifying Ownership, Borrowing, and Lifecycles (30 min) + +This is the heart of Rust's memory safety guarantees. Don't worry if it doesn't click instantly; it takes practice\! The goal here is to understand the core rules and *why* they exist. + +**The Core Rules of Ownership:** + +1. **Each value in Rust has a variable that's called its *owner*.** +2. **There can only be one owner at a time.** +3. **When the owner goes out of scope, the value will be *dropped* (memory is freed).** + +Let's see what this means in practice. + +#### **Ownership and Moves:** + +For complex data types (like `String` or `Vec`), when you assign one variable to another, Rust doesn't copy the data. Instead, it **moves** ownership. The original variable becomes invalid. + +```rust +fn main() { + let s1 = String::from("hello"); // s1 owns the String data + println!("s1 initially: {}", s1); + + let s2 = s1; // Ownership of the String data MOVES from s1 to s2 + println!("s2: {}", s2); + + // println!("s1 after move: {}", s1); // ERROR! s1 is no longer valid here. + // Rust prevents you from using s1 after it's moved. + // This prevents "use-after-free" bugs. +} +``` + + * **Why?** If `s1` and `s2` both owned the data, they might try to free it twice when they go out of scope, leading to a crash. Rust's ownership system prevents this. + +#### **The `Copy` Trait:** + +For simple, fixed-size types (like integers, booleans, characters, fixed-size arrays), Rust implements the `Copy` trait. When you assign these, the value is actually copied, and the original variable remains valid. + +```rust +fn main() { + let x = 5; // x owns the integer 5 + let y = x; // The integer 5 is COPIED. x is still valid. + + println!("x: {}, y: {}", x, y); // Output: x: 5, y: 5 +} +``` + +#### **Borrowing (References):** + +What if you want to use a value without taking ownership? You **borrow** it by creating a **reference**. References don't take ownership, so they don't free the memory when they go out of scope. + + * **Immutable References (`&`):** You can have multiple immutable references to a piece of data at the same time. This is like lending someone a book to read – many people can read it simultaneously. + + ```rust + fn main() { + let s = String::from("hello"); // s owns the String + + let len = calculate_length(&s); // Pass an immutable reference (&s) + println!("The length of '{}' is {}.", s, len); // s is still valid here! + + // You can have multiple immutable references: + let r1 = &s; + let r2 = &s; + println!("r1: {}, r2: {}", r1, r2); // Both r1 and r2 are valid + } + + fn calculate_length(s: &String) -> usize { // Function takes an immutable reference + s.len() // .len() method returns the length + } // s goes out of scope here, but it didn't own the String, so nothing is dropped. + ``` + + * **Mutable References (`&mut`):** You can have **only one mutable reference** to a piece of data at a time. This is like lending someone a book to edit – only one person can edit it at a time to avoid conflicts. + + ```rust + fn main() { + let mut s = String::from("hello"); // s must be mutable to get a mutable reference + + change_string(&mut s); // Pass a mutable reference (&mut s) + println!("Modified string: {}", s); // s is still valid and modified! + + // let r1 = &mut s; // First mutable reference + // let r2 = &mut s; // ERROR! Cannot have two mutable references at once + // println!("{}, {}", r1, r2); + + // let r1 = &s; // Immutable reference + // let r2 = &mut s; // ERROR! Cannot have a mutable and immutable reference at the same time + } + + fn change_string(some_string: &mut String) { // Function takes a mutable reference + some_string.push_str(", world!"); // Modify the string through the reference + } + ``` + +#### **Lifetimes (High-Level):** + +Lifetimes are a concept the Rust compiler uses to ensure that all references are valid for as long as they are used. They prevent "dangling references" (references pointing to data that has already been freed). + + * You'll often see them implicitly handled by the compiler. + * Sometimes, for functions that take references and return references, you might need to explicitly annotate lifetimes (e.g., `fn longest<'a>(x: &'a str, y: &'a str) -> &'a str`). + * **Key takeaway:** Lifetimes are about ensuring that data lives as long as any reference to it exists. The compiler checks this for you\! + +#### **The Borrow Checker:** + +Rust's compiler includes a "Borrow Checker." This is the part that enforces all the ownership and borrowing rules *at compile time*. If your code violates a rule, the Borrow Checker will prevent it from compiling, giving you helpful error messages. This is Rust's way of guaranteeing memory safety without a runtime garbage collector. + +**Practice:** The best way to understand Ownership is to write code and let the Borrow Checker guide you. Try to break the rules (e.g., try to use a moved variable, create two mutable references) and understand the compiler errors. + +----- + +### 3.2 Structuring Data with `struct`s (30 min) + +`struct`s (short for "structures") allow you to create custom data types by grouping related data together. They are similar to classes in object-oriented languages or objects/dictionaries in JavaScript/Python, but without built-in methods initially. + +#### **Defining a `struct`:** + +You define a `struct` using the `struct` keyword, followed by its name (typically `PascalCase`), and then curly braces containing its fields (each with a name and a type). + +```rust +// Define a struct named 'User' +struct User { + active: bool, + username: String, + email: String, + sign_in_count: u64, +} + +fn main() { + // Creating an instance of a struct + let user1 = User { // Order of fields doesn't matter + active: true, + username: String::from("alice123"), + email: String::from("alice@example.com"), + sign_in_count: 1, + }; + + // Accessing values using dot notation + println!("User 1 Name: {}", user1.username); + println!("User 1 Email: {}", user1.email); + + // To modify a field, the struct instance itself must be mutable + let mut user2 = User { + active: false, + username: String::from("bob456"), + email: String::from("bob@example.com"), + sign_in_count: 5, + }; + + user2.email = String::from("new_bob@example.com"); // This is allowed + println!("User 2 New Email: {}", user2.email); + + // You can also create new instances from existing ones using the struct update syntax + let user3 = User { + email: String::from("charlie@example.com"), + username: String::from("charlie789"), + ..user1 // Fills remaining fields from user1 (active, sign_in_count) + }; + println!("User 3 Name: {}, Active: {}", user3.username, user3.active); +} +``` + +#### **Tuple Structs:** + +Tuple structs are like tuples but have a name. They are useful when you want to give a name to a tuple but don't need named fields. + +```rust +struct Color(i32, i32, i32); // RGB values +struct Point(i32, i32, i32); // X, Y, Z coordinates + +fn main() { + let black = Color(0, 0, 0); + let origin = Point(0, 0, 0); + + println!("Black RGB: ({}, {}, {})", black.0, black.1, black.2); + // Note: black.0 is the first element, black.1 the second, etc. +} +``` + +#### **Unit-Like Structs:** + +These are useful when you need to implement a trait on some type but don't have any data that you want to store inside the type itself. + +```rust +struct AlwaysEqual; // No fields + +fn main() { + let subject = AlwaysEqual; + // You can use it as a type, but it holds no data. +} +``` + +#### **Associated Functions (Methods) using `impl`:** + +You can define functions that belong to a `struct` using an `impl` (implementation) block. These are often called **methods** if their first parameter is `self` (a reference to the instance of the struct). + +```rust +struct Rectangle { + width: u32, + height: u32, +} + +// Implement methods for the Rectangle struct +impl Rectangle { + // A method that takes an immutable reference to self + fn area(&self) -> u32 { + self.width * self.height + } + + // A method that takes a mutable reference to self + fn scale(&mut self, factor: u32) { + self.width *= factor; + self.height *= factor; + } + + // An associated function (not a method, doesn't take self) + // Often used as constructors (like 'new' in other languages) + fn square(size: u32) -> Rectangle { + Rectangle { + width: size, + height: size, + } + } +} + +fn main() { + let rect1 = Rectangle { width: 30, height: 50 }; + println!("The area of the rectangle is {} square pixels.", rect1.area()); // Call method + + let mut rect2 = Rectangle { width: 10, height: 20 }; + rect2.scale(2); // Call mutable method + println!("Scaled rectangle: width={}, height={}", rect2.width, rect2.height); + + let sq = Rectangle::square(25); // Call associated function using :: + println!("Square area: {}", sq.area()); +} +``` + +#### **Printing Structs with `Debug` Trait:** + +By default, `println!` cannot directly print structs in a readable format. You need to derive the `Debug` trait for your struct using `#[derive(Debug)]`. + +```rust +#[derive(Debug)] // Add this line above your struct definition +struct User { + active: bool, + username: String, + email: String, + sign_in_count: u64, +} + +fn main() { + let user1 = User { + active: true, + username: String::from("alice123"), + email: String::from("alice@example.com"), + sign_in_count: 1, + }; + + println!("User 1: {:?}", user1); // Use {:?} for debug printing + println!("User 1 (pretty print): {:#?}", user1); // Use {:#?} for pretty printing +} +``` + +----- + +### 3.3 Modeling Data with `enum`s (15 min) + +`enum`s (enumerations) allow you to define a type by enumerating its possible variants. In Rust, `enum`s are much more powerful than in many other languages; they are "sum types," meaning a value of an `enum` can be *one of* a set of defined possibilities. + +#### **Simple `enum`s:** + +You've already seen `Ordering` in the guessing game, which is a simple enum. + +```rust +enum TrafficLight { + Red, + Yellow, + Green, +} + +fn main() { + let current_light = TrafficLight::Red; + + match current_light { // Often used with 'match' for exhaustive handling + TrafficLight::Red => println!("Stop!"), + TrafficLight::Yellow => println!("Prepare to stop!"), + TrafficLight::Green => println!("Go!"), + } +} +``` + +#### **`enum`s with Associated Data:** + +This is where Rust's enums become extremely powerful. Each variant of an enum can hold its own specific data. + +```rust +enum Message { + Quit, // No data + Move { x: i32, y: i32 }, // Anonymous struct-like data + Write(String), // Single String data + ChangeColor(i32, i32, i32), // Tuple-like data (RGB values) +} + +fn main() { + let m1 = Message::Quit; + let m2 = Message::Move { x: 10, y: 20 }; + let m3 = Message::Write(String::from("hello")); + let m4 = Message::ChangeColor(255, 0, 128); + + // Using match to destructure and handle different enum variants + match m2 { + Message::Quit => println!("The Quit message has no data."), + Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y), + Message::Write(text) => println!("Write message: {}", text), + Message::ChangeColor(r, g, b) => println!("Change color to R:{}, G:{}, B:{}", r, g, b), + } +} +``` + +#### **The `Option` Enum (Handling Absence of a Value):** + +`Option` is a standard library enum that represents a value that might or might not be present. It's Rust's way of handling null/nil without null pointer exceptions. + +```rust +enum Option { // Conceptual definition + None, // Represents no value + Some(T), // Represents a value of type T +} + +fn main() { + let some_number = Some(5); // A value is present + let no_number: Option = None; // No value is present + + // You MUST use match (or other Option methods) to safely get the value out + match some_number { + Some(value) => println!("We have a number: {}", value), + None => println!("No number here."), + } + + match no_number { + Some(value) => println!("We have a number: {}", value), + None => println!("No number here."), + } +} +``` + +#### **The `Result` Enum (Handling Recoverable Errors):** + +`Result` is another fundamental enum for handling operations that can succeed or fail. You saw it with `read_line()` in Lesson 1. + +```rust +enum Result { // Conceptual definition + Ok(T), // Represents success, holding a value of type T + Err(E), // Represents failure, holding an error of type E +} + +fn main() { + // Example: A function that might fail + fn divide(numerator: f64, denominator: f64) -> Result { + if denominator == 0.0 { + Err(String::from("Cannot divide by zero!")) + } else { + Ok(numerator / denominator) + } + } + + let division_result = divide(10.0, 2.0); + match division_result { + Ok(value) => println!("Division successful: {}", value), + Err(error) => println!("Division failed: {}", error), + } + + let division_by_zero = divide(10.0, 0.0); + match division_by_zero { + Ok(value) => println!("Division successful: {}", value), + Err(error) => println!("Division failed: {}", error), + } +} +``` + + * **Key takeaway for `Option` and `Result`:** Rust forces you to explicitly handle the possibility of a value being absent or an operation failing, leading to more robust code. + +----- + +### 3.4 Organizing Code with Modules (15 min) + +As your Rust projects grow, you'll want to organize your code into logical units. Rust's module system helps you do this. + + * **Modules (`mod`):** Modules are like namespaces or containers for functions, structs, enums, and other modules. They help prevent naming conflicts and control visibility. + * **`use` Keyword:** The `use` keyword brings items from modules into your current scope, so you don't have to type the full path every time. + * **`pub` Keyword:** By default, everything in Rust is private. To make an item (function, struct, enum, module) visible outside its current module, you need to mark it with `pub` (public). + +#### **Example: Single File Module** + +```rust +// src/main.rs + +// Declare a module named 'greetings' +mod greetings { + // This function is private by default + fn english() { + println!("Hello!"); + } + + // This function is public, so it can be accessed from outside 'greetings' + pub fn spanish() { + println!("¡Hola!"); + } + + // Declare a nested module + pub mod formal { + pub fn english_formal() { + println!("Good day!"); + } + } +} + +fn main() { + // Call a public function from the greetings module + greetings::spanish(); + + // Call a public function from the nested formal module + greetings::formal::english_formal(); + + // greetings::english(); // ERROR! 'english' is private +} +``` + +#### **Example: Modules in Separate Files** + +For larger projects, you'll put modules in separate files. + +1. **Create a new Cargo project:** `cargo new my_app_modules` + +2. **Create module files:** + + * Inside `src/`, create a file named `greetings.rs`. + * Inside `src/greetings/`, create a file named `formal.rs`. (You'll need to create the `greetings` folder first). + +3. **Content of `src/greetings.rs`:** + + ```rust + // src/greetings.rs + pub fn spanish() { + println!("¡Hola desde el módulo de saludos!"); + } + + // Declare the nested module 'formal' + pub mod formal; + ``` + +4. **Content of `src/greetings/formal.rs`:** + + ```rust + // src/greetings/formal.rs + pub fn english_formal() { + println!("Good day from the formal submodule!"); + } + ``` + +5. **Content of `src/main.rs`:** + + ```rust + // src/main.rs + + mod greetings; // Declare the 'greetings' module (Rust looks for src/greetings.rs or src/greetings/mod.rs) + + // Bring specific items into scope using 'use' for easier access + use greetings::spanish; + use greetings::formal::english_formal; + + fn main() { + spanish(); // Now you can call it directly + english_formal(); // And this one too + + // You can still use the full path if you prefer + greetings::spanish(); + } + ``` + + + + * **`mod` declaration:** When you write `mod greetings;` in `main.rs`, Rust looks for `src/greetings.rs` or `src/greetings/mod.rs`. + * **`pub` for Visibility:** Remember to use `pub` on items you want to expose from a module. + * **`use` for Convenience:** `use` statements are like shortcuts; they don't change visibility but make names easier to type. + +----- + +**End of Lesson 3.** You've now tackled Rust's most advanced core concepts: Ownership, Structs, Enums, and Modules. These are the building blocks for writing robust and well-organized Rust applications. You're now ready to start building graphical user interfaces with Slint\! \ No newline at end of file diff --git a/docs/desktop_applications/lesson4.md b/docs/desktop_applications/lesson4.md new file mode 100644 index 0000000..a00d674 --- /dev/null +++ b/docs/desktop_applications/lesson4.md @@ -0,0 +1,418 @@ +--- +title: Building Static User Interfaces +sidebar_position: 4 +--- + +--- + +# Lesson 4: Introduction to Slint - Building Static User Interfaces + +Welcome to the exciting part where we start building actual graphical user interfaces\! You've now got a solid foundation in Rust. In this module, we'll introduce **Slint**, our chosen UI framework, and get your very first desktop application up and running. We'll focus on creating static UIs – meaning, they look good but don't do much yet\! + +--- + +### 4.1 Introducing Slint: A Declarative UI Framework (5 min) + +So, what exactly is Slint? 🤔 + +Slint is a **declarative UI toolkit** that helps you build native desktop applications. Think of it as a way to describe _what_ your user interface should look like, rather than telling the computer _how_ to draw every single pixel. + +- **Declarative:** Instead of step-by-step instructions (e.g., "create a button, set its text, position it at X,Y"), you simply declare the desired state of your UI (e.g., "there's a button with this text, inside this box"). Slint handles the "how" for you. This is similar to how React works on the web\! +- **Cross-Platform:** Slint apps can run on Windows, macOS, Linux, and even embedded systems, all from the same codebase. +- **Native Performance:** Unlike web-based desktop frameworks (like Electron, which uses a web browser to render UI), Slint compiles your UI design directly to highly optimized machine code. This results in incredibly fast and responsive applications that feel truly native. +- **Language Agnostic (but we're using Rust\!):** While Slint has APIs for Rust, C++, and JavaScript, we'll be focusing on its excellent Rust integration, leveraging all the safety and performance benefits you've just learned. + +--- + +### 4.2 Synergies: Why Slint is an Ideal Partner for Rust (5 min) + +Rust and Slint are a fantastic combination for desktop app development, creating a powerful synergy: + +1. **Performance Meets UI:** Rust's blazing-fast execution speed ensures your UI is always smooth and responsive, even with complex operations. Slint's native rendering complements this perfectly. +2. **Safety from Backend to Frontend:** Rust's memory safety and concurrency guarantees extend to your UI logic. You can build complex, multi-threaded applications with confidence, knowing that common bugs like data races are prevented at compile time. +3. **Declarative Simplicity:** Slint's declarative `.slint` language makes UI design intuitive and easy to read, while Rust handles the robust backend logic. This separation of concerns helps keep your codebase clean. +4. **Modern Development Experience:** With Cargo for Rust and Slint's live-preview tools (which we'll see later), the development workflow is efficient and enjoyable. + +--- + +### 4.3 Setting Up Your First Slint Application (20 min) + +Let's get a basic Slint project initialized and running\! 🚀 + +1. **Create a New Cargo Project:** + + - Open your terminal and navigate to your development directory. + - Create a new Rust project, just like before: + ```bash + cargo new my_slint_app + ``` + - Navigate into the new project folder: + ```bash + cd my_slint_app + ``` + +2. **Add Slint as a Dependency:** + + - Open your `Cargo.toml` file in VS Code. + - Under the `[dependencies]` section, add the `slint` crate. You'll also need `slint-build` as a build dependency, which is used by Cargo to compile your `.slint` files. + - Your `Cargo.toml` should look something like this (versions might differ slightly, use the latest stable `1.x`): + + ```toml + # Cargo.toml + [package] + name = "my_slint_app" + version = "0.1.0" + edition = "2021" + + [dependencies] + slint = "1.x" # Use the latest stable version, e.g., "1.4" + + [build-dependencies] + slint-build = "1.x" # Use the same version as slint + ``` + + - **Save `Cargo.toml`\!** + +3. **Create Your `.slint` UI File:** + + - In the root of your `my_slint_app` folder (the same level as `src` and `Cargo.toml`), create a new file named `ui.slint`. + - This file will contain the declarative description of your UI. + +4. **Write Basic `.slint` UI Code:** + + - Open `ui.slint` and add this simple code: + + ```slint + // ui.slint + export component MainWindow inherits Window { + // This defines our main window. 'export' makes it visible to Rust. + // 'inherits Window' means it's a top-level window. + width: 300px; // Set the initial width of the window + height: 200px; // Set the initial height of the window + + // Add a simple Text element inside the window + Text { + text: "Hello, Slint!"; // The text content + color: #2196F3; // A nice blue color + font-size: 24px; // Font size + horizontal-alignment: center; // Center horizontally + vertical-alignment: center; // Center vertically + } + } + ``` + + - **Save `ui.slint`\!** + +5. **Update `src/main.rs`:** + + - Open `src/main.rs` and replace its content with the following. This Rust code will load and run your Slint UI. + + ```rust + // src/main.rs + + // This macro tells Slint to compile the 'ui.slint' file + // and generate Rust code for the UI components defined within it. + slint::include_modules!(); + + fn main() { + // Create an instance of our MainWindow component defined in ui.slint. + // Slint generates a struct named 'MainWindow' for us. + let main_window = MainWindow::new().unwrap(); + + // Run the application's event loop. This displays the window + // and keeps the application running until the window is closed. + main_window.run().unwrap(); + } + ``` + + - **Save `src/main.rs`\!** + +6. **Run Your Slint Application:** + + - In your terminal (still in the `my_slint_app` folder), run: + ```bash + cargo run + ``` + - You should see a new desktop window pop up with "Hello, Slint\!" displayed in blue\! + +Congratulations\! You've just built and run your first graphical desktop application using Rust and Slint\! 🎉 + +--- + +### 4.4 Understanding the `.slint` Language and Basic UI Syntax (15 min) + +The `.slint` file is where you define your UI's structure and appearance. It's a **declarative language** specifically designed for describing user interfaces, making it easy to read and understand at a glance. + +- **Declarative Nature:** You state _what_ you want to see, not _how_ to draw it. +- **Components:** The basic building blocks are `component`s. Every UI element you define (like `MainWindow` in our example) is a component. + - `export component MyComponent inherits BaseComponent { ... }` + - `export`: Makes the component visible to your Rust code. + - `inherits Window`: Means it's a top-level window. Other components might inherit `Rectangle` or `VerticalBox`. +- **Elements:** Inside components, you place UI elements. These can be built-in elements (like `Text`, `Rectangle`, `Button`) or other custom components you define. + - `ElementName { property: value; another-property: another-value; }` +- **Properties:** Elements and components have properties that control their appearance and behavior. + - `width: 300px;` + - `color: #FF0000;` + - `text: "Some text";` + - Units like `px` (pixels) are common. +- **Nesting:** You nest elements inside curly braces `{}` to create parent-child relationships, forming the UI hierarchy. The child element is drawn _inside_ its parent. + +**Example Snippet from `ui.slint`:** + +```slint +// Defining a reusable component called MyButton +export component MyButton inherits Rectangle { + // An 'in' property means data comes into this component from its parent + in property button_text; + + // Child elements that make up the button's visual + Rectangle { // A background rectangle for the button + background: blue; + border-radius: 5px; + } + Text { // The text displayed on the button + text: button_text; // This text is bound to the 'button_text' property + color: white; + horizontal-alignment: center; + vertical-alignment: center; + } +} +``` + +--- + +### 4.5 Core Slint UI Elements (20 min) + +Slint provides a set of standard UI elements (often called "widgets") that you'll use to build your interfaces. These are typically imported from `std-widgets.slint`. + +To use them, you usually add an `import` statement at the top of your `.slint` file: +`import { Button, LineEdit, VerticalBox } from "std-widgets.slint";` + +Here are some of the most common ones: + +- **`Window`**: The top-level container for your application. Every Slint application has at least one. +- **`Rectangle`**: A basic rectangular shape. Very versatile for backgrounds, borders, or as a base for custom components. + - **Properties:** `width`, `height`, `background`, `border-radius`, `border-width`, `border-color`, `x`, `y` (for positioning). +- **`Text`**: Displays static or dynamic text. + - **Properties:** `text`, `color`, `font-size`, `font-weight`, `horizontal-alignment`, `vertical-alignment`. +- **`Button`**: A clickable button. + - **Properties:** `text`, `enabled` (boolean). + - **Callbacks (events):** `clicked => { ... }` (we'll cover events in the next lesson). +- **`LineEdit`**: A single-line text input field. + - **Properties:** `text`, `placeholder-text`, `read-only`. +- **`Image`**: Displays an image. + - **Properties:** `source` (path to image file), `width`, `height`, `fill`. +- **`CheckBox`**: A toggleable checkbox. + - **Properties:** `checked` (boolean), `text`. +- **`ComboBox`**: A dropdown selection list. + - **Properties:** `model` (for list items), `current-value`. + +**Example:** Let's create a simple login form using some of these elements, for now, we'll use basic `x` and `y` coordinates for positioning. + +```slint +// ui.slint (add these imports if not already there) +import { Button, LineEdit, CheckBox, Text, Rectangle } from "std-widgets.slint"; + +export component MyStaticElements inherits Window { + width: 400px; height: 300px; + title: "Static Elements Demo"; + + // A background rectangle for the window + Rectangle { + width: 100%; height: 100%; + background: #ECEFF1; // Light grey background + } + + Text { + text: "User Login"; + font-size: 28px; + font-weight: 600; + color: #37474F; + x: 20px; y: 20px; // Absolute positioning + } + + LineEdit { + placeholder-text: "Username"; + width: 200px; height: 30px; + x: 20px; y: 70px; + } + + LineEdit { + placeholder-text: "Password"; + width: 200px; height: 30px; + x: 20px; y: 110px; + // input-type: password; // For password masking (more advanced) + } + + CheckBox { + text: "Remember me"; + x: 20px; y: 150px; + checked: true; // Static for now + } + + Button { + text: "Login"; + x: 20px; y: 190px; + width: 100px; height: 35px; + background: #4CAF50; // Green button + color: white; + } +} +``` + +- **Note on Positioning:** For now, we're using `x` and `y` for absolute positioning. This is generally not ideal for responsive UIs. We'll fix this with layout managers next\! + +--- + +### 4.6 Structuring Your UI: Essential Layout Managers (15 min) + +Absolute positioning with `x` and `y` is rigid and doesn't adapt well to different screen sizes or content changes. Slint provides **layout managers** to arrange elements dynamically and responsively, making your UIs much more flexible. + +- **`HorizontalBox`**: Arranges children side-by-side in a horizontal row. + - **Properties:** `spacing` (space between children), `alignment` (vertical alignment of children within the box). +- **`VerticalBox`**: Arranges children one below another in a vertical column. + - **Properties:** `spacing` (space between children), `alignment` (horizontal alignment of children within the box). +- **`GridBox`**: Arranges children in a grid (rows and columns), giving you precise control over placement. + - **Properties:** `columns`, `rows`, `column-spacing`, `row-spacing`. + - **Children properties:** `row`, `column`, `row-span`, `column-span`. + +**Example: Rebuilding the Login Form with Layouts** + +Let's refactor our previous login form to use a `VerticalBox` for a much cleaner and more adaptable layout. + +```slint +// ui.slint (add these imports if not already there) +import { Button, LineEdit, CheckBox, Text, Rectangle, VerticalBox } from "std-widgets.slint"; + +export component MyLayoutsDemo inherits Window { + width: 400px; height: 300px; + title: "Layouts Demo"; + + // Use a VerticalBox as the main container for vertical stacking + VerticalBox { + spacing: 15px; // Space between items in the box + alignment: center; // Center children horizontally within the box + padding: 20px; // Padding inside the box + + // Background for the entire window + Rectangle { + width: 100%; height: 100%; // Fill the parent VerticalBox + background: #ECEFF1; // Light grey background + // This rectangle is here just to show the background, it will be behind the content + // You might remove it if the Window itself has a background property. + } + + Text { + text: "User Login"; + font-size: 28px; + font-weight: 600; + color: #37474F; + horizontal-alignment: center; // Center text within its own space + } + + LineEdit { + placeholder-text: "Username"; + width: 250px; // Give it a fixed width for now + } + + LineEdit { + placeholder-text: "Password"; + width: 250px; + } + + CheckBox { + text: "Remember me"; + // Checkboxes align to the start by default in VerticalBox, + // but 'alignment: center' on the parent VerticalBox will center it. + } + + Button { + text: "Login"; + width: 150px; // Give the button a fixed width + height: 35px; + background: #4CAF50; + color: white; + } + } +} +``` + +- **Observation:** Notice how much cleaner the layout code is. You don't specify `x` or `y` anymore. The `VerticalBox` automatically positions the elements, making your UI much more maintainable and adaptable. + +--- + +### 4.7 Developing Your First Static Desktop Application (10 min) + +For this final part of the lesson, your task is to combine everything you've learned about Slint to create a simple, static desktop application. This will be your first complete Slint UI\! + +**Exercise: Create a simple "About Us" window for an imaginary application.** + +- **Requirements:** + - A `Window` with a clear title (e.g., "About My Awesome App"). + - Use a **layout manager** (e.g., `VerticalBox`) for overall arrangement. + - Include at least two `Text` elements: one for a main heading and another for a descriptive paragraph. + - Add an `Image` element (you can use a placeholder image, but for simplicity, just define the `Image` element with a `width` and `height` and a dummy `source` path like `"assets/logo.png"`). + - Include a `Button` (it won't do anything yet, but it should be visible). + - Apply some basic styling (colors, font sizes, padding) to make it look visually appealing. + +**Steps:** + +1. Open your `my_slint_app` project (or create a new one: `cargo new about_app`). +2. Modify your `ui.slint` file to define your `AboutWindow` component. +3. Modify `src/main.rs` to load and run your new `AboutWindow` component (if you created a new project). +4. Run `cargo run` in your terminal to see your beautiful static application\! + +**Example `ui.slint` structure (Don't copy-paste\! Build it yourself using the concepts\!):** + +```slint +// ui.slint (for About Us app) +import { Button, Text, Image, VerticalBox } from "std-widgets.slint"; + +export component AboutWindow inherits Window { + width: 500px; + height: 400px; + title: "About My Awesome App"; + + VerticalBox { + spacing: 10px; + alignment: center; // Center content horizontally + padding: 20px; + + Text { + text: "My Awesome App"; + font-size: 36px; + font-weight: 700; + color: #3F51B5; // Indigo color + horizontal-alignment: center; + } + + Image { + source: "assets/app_logo.png"; // Placeholder for an image file + width: 100px; + height: 100px; + } + + Text { + text: "Version 1.0.0\n\nDeveloped by Your Name/Team\n\nThis application is designed to help you organize your daily tasks and boost your productivity. We believe in simple, efficient, and beautiful software."; + font-size: 16px; + color: #424242; + horizontal-alignment: center; + wrap: word-wrap; // Allow text to wrap within its bounds + } + + Button { + text: "Close"; + width: 120px; + height: 40px; + background: #F44336; // Red button + color: white; + } + } +} +``` + +(Remember to create an `assets` folder in your project root and put a placeholder image there, or just comment out the `Image` element if you don't have one ready.) + +--- + +**End of Lesson 4.** You've successfully entered the world of GUI development with Slint\! You can now set up a project, define UI elements, arrange them with layouts, and create static desktop applications. Next, we'll make them interactive\! diff --git a/docs/desktop_applications/lesson5.md b/docs/desktop_applications/lesson5.md new file mode 100644 index 0000000..994e384 --- /dev/null +++ b/docs/desktop_applications/lesson5.md @@ -0,0 +1,327 @@ +--- +title: State, Events, and Dynamic UIs +sidebar_position: 5 +--- + +--- + +# Lesson 5: Slint Interactivity - State, Events, and Dynamic UIs + +Welcome to the heart of UI development\! Static applications are cool, but truly useful apps need to react to user input and display changing information. In this module, we'll make our Slint applications come alive by diving into **state management**, **data binding**, and **event handling**. + +--- + +### 5.1 State Management and Data Binding (30 min) + +In UI development, "state" refers to any data that can change over time and affect what the user sees. "Data binding" is the magic that connects your UI elements directly to this state, so when the state changes, the UI automatically updates. + +Slint's approach to reactivity is very powerful: when a property's value changes, any UI element or other property that depends on it automatically updates. You don't have to manually tell the UI to redraw\! + +#### **Defining Properties in `.slint`:** + +You declare properties within your components using the `property` keyword. These properties are where you store your component's state. + +- **`in property name;`**: An "in" property means data flows _into_ this component from its parent. It's usually read-only within the component. +- **`out property name;`**: An "out" property means data flows _out_ of this component to its parent. It's usually set by the component and read by the parent. +- **`in-out property name;`**: An "in-out" property means data can flow both ways. It can be set by the parent and modified by the component itself. This is great for two-way binding. + +**Example: A Simple Counter** + +Let's create a counter that displays a number. + +```slint +// ui.slint +export component CounterApp inherits Window { + width: 200px; + height: 150px; + title: "Simple Counter"; + + // 1. Define an 'in-out' property for our counter's value + in-out property counter: 0; // Initial value is 0 + + VerticalBox { + alignment: center; + spacing: 10px; + + Text { + // 2. Bind the text property directly to our 'counter' property + // When 'counter' changes, this text will automatically update! + text: "Count: " + counter; + font-size: 30px; + color: #4CAF50; + } + + Button { + text: "Increase"; + width: 100px; + height: 40px; + background: #2196F3; + color: white; + // We'll add the 'clicked' event logic in the next section! + } + } +} +``` + +#### **Binding UI to Data:** + +You saw this in the counter example: `text: "Count: " + counter;`. This is a **binding expression**. Any time the `counter` property changes, Slint automatically re-evaluates this expression and updates the `Text` element. It's reactive\! + +#### **Updating Data from Rust (and Bidirectional Binding):** + +To make the counter actually _do_ something, we need our Rust code to modify the `counter` property. + +1. **Accessing Properties:** Slint generates methods on your component's Rust struct to get and set properties. For an `in-out` property named `my_property`, you'll typically have `get_my_property()` and `set_my_property(value)`. +2. **Bidirectional Binding (`<=>`):** For input elements like `LineEdit`, you often want the UI to update the data, and the data to update the UI. Slint's `property <=> other_property;` syntax handles this. + +**Example: Connecting Rust to the Counter** + +Let's modify `src/main.rs` to control the counter. + +```rust +// src/main.rs +slint::include_modules!(); + +fn main() { + let app = CounterApp::new().unwrap(); + + // Get a handle to the UI. This is a clone of the UI handle + // which allows us to move it into closures (like event handlers) + // without violating Rust's ownership rules. + let ui_handle = app.as_weak(); + + // Set an initial value for the counter from Rust + app.set_counter(0); + + // We'll add the button's event handler here in the next section! + // For now, imagine a timer or another event causing this: + // app.set_counter(app.get_counter() + 1); // This would update the UI + + app.run().unwrap(); +} +``` + +--- + +### 5.2 Handling User Interactions: Events and Callbacks (30 min) + +Our UI needs to respond when the user clicks a button, types in a text box, or performs other actions. Slint uses **callbacks** for this. + +#### **Defining Callbacks in `.slint`:** + +You define callbacks within your components using the `callback` keyword. These declare that a component _can_ trigger a specific action. + +- `callback my_action();` (no arguments) +- `callback value_changed(int new_value);` (with arguments) + +#### **Implementing Callback Logic in Rust:** + +In your Rust code, you connect a Rust function (a closure) to a Slint callback using the `on_callback_name()` method. + +**Example: Making the Counter Button Work** + +Let's make our "Increase" button actually increase the counter. + +```slint +// ui.slint (updated for button click) +export component CounterApp inherits Window { + width: 200px; + height: 150px; + title: "Simple Counter"; + + in-out property counter: 0; + + // Define a callback that the UI can trigger + callback request_increase(); // This callback will be called when the button is clicked + + VerticalBox { + alignment: center; + spacing: 10px; + + Text { + text: "Count: " + counter; + font-size: 30px; + color: #4CAF50; + } + + Button { + text: "Increase"; + width: 100px; + height: 40px; + background: #2196F3; + color: white; + // When this button is clicked, trigger our 'request_increase' callback + clicked => { root.request_increase(); } + } + } +} +``` + +Now, update `src/main.rs` to handle the `request_increase` callback: + +```rust +// src/main.rs +slint::include_modules!(); + +fn main() { + let app = CounterApp::new().unwrap(); + + // Get a weak handle to the UI. This is crucial for event handlers + // to avoid creating reference cycles (which can cause memory leaks) + // and to allow the UI to be dropped when the app closes. + let ui_handle = app.as_weak(); + + app.set_counter(0); // Set initial counter value + + // Connect our Rust logic to the Slint callback + app.on_request_increase(move || { // 'move' captures variables by value into the closure + // We need to unwrap the weak handle to get a strong handle back. + // If the UI has been dropped (e.g., window closed), ui_handle.upgrade() will return None. + if let Some(ui) = ui_handle.upgrade() { + // Get the current counter value, increment it, and set it back. + ui.set_counter(ui.get_counter() + 1); + } + }); + + app.run().unwrap(); +} +``` + +#### **Error Handling in Callbacks:** + +In real applications, your callback logic might involve operations that can fail (like file I/O or network requests). You should handle these `Result` types gracefully. + +- You can use `match` or `if let Ok`/`if let Err` within your callback closures. +- For simplicity in basic examples, you might use `.unwrap()` or `.expect()`, but remember their limitations (they panic on `Err`). + +**Example (Conceptual):** + +```rust +// Inside an app.on_some_action(move || { ... }) callback +if let Some(ui) = ui_handle.upgrade() { + let result = some_function_that_might_fail(); // Returns a Result + match result { + Ok(data) => { + ui.set_some_property(data); + }, + Err(e) => { + // Display an error message in the UI or log it + eprintln!("An error occurred: {:?}", e); + ui.set_status_message(format!("Error: {}", e).into()); + } + } +} +``` + +--- + +### 5.3 Practical Application: Developing an Interactive Calculator Application (30 min) + +Now, let's combine everything you've learned to build a fully interactive, basic calculator application\! This will be a great way to solidify your understanding of properties, data binding, and events. + +**Calculator Features:** + +1. **Display:** Shows the current input/result. +2. **Number Buttons:** 0-9. +3. **Operation Buttons:** +, -, \*, /, = (equals). +4. **Clear Button:** C. + +**Steps to Build:** + +1. **Create a new Cargo project:** `cargo new slint_calculator` + +2. **Add Slint dependency:** Update `Cargo.toml` with `slint = "1.x"` and `slint-build = "1.x"` as before. + +3. **Design the UI in `ui.slint`:** + + - Use a `VerticalBox` as the main container. + - Use a `LineEdit` for the display (read-only). + - Use `GridBox` for the calculator buttons (numbers and operations). + - Place buttons for 0-9, +, -, \*, /, =, C. + - Give your `LineEdit` a property (e.g., `in-out property display_text;`) and bind its `text` property to it. + - For each button, define a `callback` (e.g., `callback digit_pressed(string digit);`, `callback operation_pressed(string op);`, `callback clear_pressed();`, `callback equals_pressed();`). + - Inside each button's `clicked => { ... }` handler, call the appropriate `root.callback_name(...)`. + +4. **Implement the Logic in `src/main.rs`:** + + - Load your `ui.slint` using `slint::include_modules!()`. + - Create an instance of your main calculator component. + - Maintain internal state in Rust (e.g., `current_number: String`, `first_operand: f64`, `operator: Option`). + - Implement the `on_digit_pressed`, `on_operation_pressed`, `on_clear_pressed`, `on_equals_pressed` callbacks. + - In these callbacks, update the internal state and then update the Slint UI's `display_text` property using `ui.set_display_text(...)`. + - **Hint:** For `parse()` operations on numbers, remember to handle the `Result` type (e.g., with `match` or `.unwrap_or_default()` for simplicity in a calculator). + +**Example `ui.slint` structure for the calculator (build it, don't just copy\!):** + +```slint +// ui.slint (for Calculator) +import { Button, LineEdit, VerticalBox, GridBox } from "std-widgets.slint"; + +export component CalculatorApp inherits Window { + width: 300px; + height: 400px; + title: "Rust Slint Calculator"; + + in-out property display_text: "0"; // The text shown on the calculator screen + + // Callbacks for different button types + callback digit_pressed(string digit); + callback operation_pressed(string op); + callback clear_pressed(); + callback equals_pressed(); + + VerticalBox { + alignment: center; + spacing: 5px; + padding: 10px; + + LineEdit { + read-only: true; // Users can't type directly + text: display_text; // Bind to our display property + font-size: 36px; + horizontal-alignment: right; + height: 60px; + background: #CFD8DC; // Light grey for display + border-radius: 5px; + padding-right: 10px; + } + + // Grid for calculator buttons + GridBox { + columns: 4; + rows: 4; + spacing: 5px; + + // Row 1 + Button { text: "7"; clicked => { root.digit_pressed("7"); } } + Button { text: "8"; clicked => { root.digit_pressed("8"); } } + Button { text: "9"; clicked => { root.digit_pressed("9"); } } + Button { text: "/"; clicked => { root.operation_pressed("/"); } background: #FF9800; color: white; } // Orange for operations + + // Row 2 + Button { text: "4"; clicked => { root.digit_pressed("4"); } } + Button { text: "5"; clicked => { root.digit_pressed("5"); } } + Button { text: "6"; clicked => { root.digit_pressed("6"); } } + Button { text: "*"; clicked => { root.operation_pressed("*"); } background: #FF9800; color: white; } + + // Row 3 + Button { text: "1"; clicked => { root.digit_pressed("1"); } } + Button { text: "2"; clicked => { root.digit_pressed("2"); } } + Button { text: "3"; clicked => { root.digit_pressed("3"); } } + Button { text: "-"; clicked => { root.operation_pressed("-"); } background: #FF9800; color: white; } + + // Row 4 + Button { text: "C"; clicked => { root.clear_pressed(); } background: #F44336; color: white; } // Red for clear + Button { text: "0"; clicked => { root.digit_pressed("0"); } } + Button { text: "="; clicked => { root.equals_pressed(); } background: #4CAF50; color: white; } // Green for equals + Button { text: "+"; clicked => { root.operation_pressed("+"); } background: #FF9800; color: white; } + } + } +} +``` + +This project will challenge you to think about UI state, how to update it from Rust, and how to trigger Rust code from UI events. Good luck\! + +--- + +**End of Lesson 5.** You've now unlocked the power of interactive UIs with Slint\! You can manage state, bind data, and handle user events, culminating in a functional calculator application. Next up, we'll make your apps communicate with the outside world using HTTP APIs\! diff --git a/docs/desktop_applications/lesson6.md b/docs/desktop_applications/lesson6.md new file mode 100644 index 0000000..266453f --- /dev/null +++ b/docs/desktop_applications/lesson6.md @@ -0,0 +1,331 @@ +--- +title: Foundations of HTTP & Asynchronous Rust +sidebar_position: 6 +--- + +--- + +# Lesson 6: Network Communication - Foundations of HTTP & Asynchronous Rust + +Up until now, our apps have been self-contained. But real-world applications often need to fetch data from the internet (like weather updates, news, or country statistics) or send data to a server (like saving user preferences or posting messages). Today, we'll learn the fundamental principles of how computers communicate over the web and, crucially, how to do this in Rust without freezing your beautiful Slint UI. + +--- + +### 6.1 The Imperative of Inter-Computer Communication (5 min) + +Why do our applications need to talk to other computers? + +Imagine a world where every app is an island. Your weather app would only know the weather you manually typed in. Your social media app couldn't show you friends' updates. That's not very useful, right? + +- **Accessing Remote Data:** The internet is a vast repository of information (APIs\!). Our apps need to fetch this data. +- **Sharing Information:** Multiple users need to access and modify the same data (e.g., a shared to-do list, a chat application). This data lives on a central server. +- **Leveraging Services:** Apps can use specialized services running on other computers (e.g., payment processing, machine learning models, cloud storage). + +This communication is typically done over networks, and for web-based services, the **HTTP protocol** is the universal language. + +--- + +### 6.2 HTTP Protocol: Structure and Fundamentals (10 min) + +**HTTP** stands for **Hypertext Transfer Protocol**. It's the foundation of data communication for the World Wide Web. Think of it as a set of rules for how clients (like your browser or your Rust desktop app) and servers (where data lives) exchange messages. + +#### **Client-Server Model:** + +- **Client:** Your Rust desktop application (or a web browser) that _requests_ data or services. +- **Server:** A remote computer that _provides_ data or services in response to client requests. + +#### **Request-Response Cycle:** + +The core of HTTP is a simple cycle: + +1. **Client sends a Request:** Your app sends a message to the server, asking for something. +2. **Server sends a Response:** The server processes the request and sends a message back to your app. + +#### **Key Parts of an HTTP Request:** + +- **Method (Verb):** Tells the server what action you want to perform. + - **`GET`**: Retrieve data (e.g., get a list of countries). + - **`POST`**: Send new data to create a resource (e.g., create a new user). + - **`PUT`**: Send data to update an existing resource (e.g., update a user's profile). + - **`DELETE`**: Remove a resource (e.g., delete a user). + - _(These map directly to the **CRUD** operations: Create, Read, Update, Delete)_ +- **URL (Uniform Resource Locator):** The address of the resource you're interacting with (e.g., `https://restcountries.com/v3.1/all`). +- **Headers:** Key-value pairs providing metadata about the request (e.g., `Content-Type: application/json`, `Authorization: Bearer `). +- **Body (Optional):** The actual data you're sending to the server, typically for `POST` or `PUT` requests (e.g., a JSON object representing a new country). + +#### **Key Parts of an HTTP Response:** + +- **Status Code:** A three-digit number indicating the outcome of the request. + - `200 OK`: Request successful. + - `201 Created`: Resource successfully created (for `POST`). + - `204 No Content`: Request successful, but no content to return (for `DELETE`). + - `400 Bad Request`: Client sent an invalid request. + - `404 Not Found`: Resource not found on the server. + - `500 Internal Server Error`: Server encountered an error. +- **Headers:** Metadata about the response (e.g., `Content-Type: application/json`). +- **Body (Optional):** The data returned by the server (e.g., a JSON array of countries). + +--- + +### 6.3 Introduction to Asynchronous Rust (30 min) + +When your desktop application makes a network request, it has to wait for the server to respond. This waiting (called **I/O blocking**) can be a problem: if your application's main thread is waiting for the network, your UI will **freeze** and become unresponsive\! 🥶 + +**The Problem:** + +**The Solution: Asynchronous Programming\!** + +Asynchronous programming allows your program to start a long-running task (like a network request) and then _continue doing other things_ (like keeping the UI responsive) while it waits for that task to complete. When the task finishes, it notifies your program, which can then process the result. + +In Rust, we achieve this with `async` and `await` keywords, built on top of a concept called **Futures**. + +- **`async fn`:** A function declared with `async fn` is an "asynchronous function." When you call it, it doesn't immediately run all its code. Instead, it returns a `Future`. Think of a `Future` as a promise that a value _will_ be available at some point. +- **`await`:** Inside an `async fn`, you can use the `await` keyword on another `Future`. When you `await` something, your `async fn` pauses its execution _without blocking the thread_, allowing the program to work on other tasks. When the `Future` you're `await`ing resolves (e.g., the network request finishes), your `async fn` resumes from where it left off. + +#### **The Role of an Asynchronous Runtime (Tokio):** + +Rust's `async/await` syntax provides the _tools_ for asynchronous programming, but it doesn't include an "executor" or "runtime" to actually _run_ those `Future`s. You need to pick one. The most popular and powerful runtime in Rust is **Tokio**. + +Tokio is like the orchestrator that takes all your `Future`s and efficiently runs them, making sure your program doesn't block. + +#### **Setting Up Tokio:** + +1. **Add Tokio to `Cargo.toml`:** + + ```toml + # Cargo.toml + [dependencies] + # ... other dependencies ... + tokio = { version = "1", features = ["full"] } # "full" includes most common features for convenience + ``` + +2. **The `#[tokio::main]` Macro:** + This macro is the simplest way to get a Tokio runtime running for your `main` function. It transforms your `async fn main()` into a regular `fn main()` that sets up and runs the Tokio runtime. + + ```rust + // src/main.rs + #[tokio::main] // This macro sets up and runs the Tokio runtime + async fn main() { // main function is now 'async' + println!("Hello from async Rust!"); + // Your async code will go here + } + ``` + +#### **Running Tasks in the Background (`tokio::spawn`):** + +In a GUI application, your main thread is busy handling UI events. You **must not** `await` long-running tasks directly on the main UI thread, or your UI will freeze. Instead, you **spawn** these tasks onto the Tokio runtime. + +- `tokio::spawn(async { ... });` creates a new independent asynchronous task that runs in the background. It returns a `JoinHandle` which you can `await` later if you need the result, or just let it run. + +**Example: A Non-Blocking Delay** + +```rust +// src/main.rs +#[tokio::main] +async fn main() { + println!("Start of program."); + + // Spawn an asynchronous task that waits for 2 seconds + tokio::spawn(async { + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + println!("Async task finished after 2 seconds."); + }); + + println!("Program continues immediately after spawning task."); + + // In a real GUI, your Slint app.run().unwrap() would be here, + // keeping the UI responsive while the spawned task runs. + // For this console example, we'll just wait a bit longer to ensure the spawned task finishes. + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + println!("End of program."); +} +``` + +- **Run this:** `cargo run`. You'll see "Start of program." and "Program continues immediately..." almost at the same time, then after 2 seconds, "Async task finished...", and finally "End of program.". This demonstrates non-blocking behavior. + +**Key Takeaway for UIs:** For network requests in Slint, you'll typically `spawn` the `reqwest` call in an `async` block, and then use a mechanism (like a Slint callback or channel) to send the result back to the UI thread to update the display. + +--- + +### 6.4 Making External API Calls with `reqwest` and `tokio` (25 min) + +`reqwest` is the most popular and robust HTTP client library for Rust. It integrates seamlessly with Tokio for asynchronous operations. + +#### **Setting Up `reqwest`:** + +1. **Add `reqwest` to `Cargo.toml`:** + ```toml + # Cargo.toml + [dependencies] + # ... other dependencies ... + tokio = { version = "1", features = ["full"] } + reqwest = { version = "0.11", features = ["json"] } # "json" feature for easy JSON handling + ``` + - `features = ["json"]` is important as it enables `reqwest`'s convenient `.json()` method for responses and `.json(&data)` for requests. + +#### **Example: Making a `GET` Request to the Rest Countries API** + +Let's fetch data for a specific country (e.g., Japan) using `reqwest`. + +```rust +// src/main.rs +use reqwest; // The HTTP client library +use tokio; // The async runtime + +#[tokio::main] +async fn main() -> Result<(), Box> { // main now returns a Result + println!("Fetching country data..."); + + let country_code = "JPN"; // Japan's code + let url = format!("https://restcountries.com/v3.1/alpha/{}", country_code); + + // Make the GET request. .await? handles the Future and propagates errors. + let response = reqwest::get(&url).await?; + + // Check if the request was successful (2xx status code) + if response.status().is_success() { + // Get the response body as text + let body = response.text().await?; + println!("Response for {}:\n{}", country_code, body); + } else { + println!("Failed to fetch data for {}. Status: {}", country_code, response.status()); + } + + Ok(()) // Indicate success +} +``` + +- **`Result<(), Box>`:** This return type for `main` is common for async Rust examples. It means the function either succeeds (`Ok(())`) or returns an error that can be boxed (useful for diverse error types). +- **`.await?`:** This is a powerful shorthand. It's equivalent to: + ```rust + let result = some_async_operation().await; + match result { + Ok(val) => val, + Err(err) => return Err(err.into()), // Convert error and return from function + } + ``` + It allows you to write async code that looks sequential but handles errors gracefully. + +--- + +### 6.5 Data Serialization/Deserialization: Processing API Responses with `serde` (25 min) + +APIs almost always communicate using **JSON** (JavaScript Object Notation). Rust needs a way to convert this JSON text into Rust `struct`s (Deserialization) and convert Rust `struct`s into JSON text (Serialization). This is where the `serde` ecosystem comes in. + +- **`serde`**: The core serialization/deserialization framework. +- **`serde_json`**: A crate that implements `serde` for JSON data. + +#### **Setting Up `serde`:** + +1. **Add `serde` and `serde_json` to `Cargo.toml`:** + ```toml + # Cargo.toml + [dependencies] + # ... other dependencies ... + tokio = { version = "1", features = ["full"] } + reqwest = { version = "0.11", features = ["json"] } + serde = { version = "1.0", features = ["derive"] } # "derive" feature for automatic trait implementation + serde_json = "1.0" + ``` + - `features = ["derive"]` for `serde` allows you to automatically implement `Serialize` and `Deserialize` traits on your structs using `#[derive()]`. + +#### **Example: Deserializing Country Data (JSON to Rust Struct)** + +Let's define a Rust `struct` that matches the structure of the JSON response we expect from the Rest Countries API for a single country. + +```rust +// src/main.rs +use reqwest; +use tokio; +use serde::Deserialize; // Import Deserialize trait + +// Use #[derive(Debug, Deserialize)] to automatically implement traits +// Debug allows us to print the struct with {:?} +// Deserialize allows serde to convert JSON into this struct +#[derive(Debug, Deserialize)] +struct Country { + name: CountryName, + population: u64, + region: String, + languages: Option>, // Languages can be optional + capital: Option>, // Capital can be optional and an array +} + +#[derive(Debug, Deserialize)] +struct CountryName { + common: String, + official: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Fetching country data..."); + + let country_code = "JPN"; + let url = format!("https://restcountries.com/v3.1/alpha/{}", country_code); + + let client = reqwest::Client::new(); // It's good practice to reuse a reqwest client + let response = client.get(&url).send().await?; + + if response.status().is_success() { + // The Rest Countries API for /alpha/:code returns an array of countries, + // even if it's just one. So we expect a Vec. + let countries_data: Vec = response.json().await?; // Deserialize JSON directly to Vec + + if let Some(country) = countries_data.into_iter().next() { // Take the first country from the array + println!("Fetched Country: {:#?}", country); + println!("Common Name: {}", country.name.common); + println!("Population: {}", country.population); + println!("Region: {}", country.region); + + if let Some(langs) = country.languages { + println!("Languages: {:?}", langs.values().collect::>()); + } + if let Some(caps) = country.capital { + println!("Capital: {}", caps.join(", ")); + } + } else { + println!("No country data found in response."); + } + } else { + println!("Failed to fetch data for {}. Status: {}", country_code, response.status()); + } + + Ok(()) +} +``` + +- **`#[derive(Debug, Deserialize)]`**: These are procedural macros that automatically generate the necessary code for your `Country` and `CountryName` structs to be printable for debugging and deserializable from JSON. +- **`response.json().await?`**: This is a convenient `reqwest` method (enabled by the `json` feature) that attempts to parse the response body directly into the specified Rust type (here, `Vec`). It returns a `Result`. +- **`Option` and `Vec` for missing/array data:** Notice how `languages` and `capital` are wrapped in `Option` and `Vec` to correctly match the API's JSON structure, which might have missing fields or arrays. + +#### **Serialization (Rust Struct to JSON):** + +While we're focusing on calling external APIs (which means deserializing responses), you'd use `#[derive(Serialize)]` and `serde_json::to_string()` if you were building your _own_ API or sending JSON data in `POST` requests. + +```rust +use serde::Serialize; +use serde_json; + +#[derive(Debug, Serialize)] +struct NewProduct { + name: String, + price: f64, + in_stock: bool, +} + +fn main() { + let product = NewProduct { + name: "Rust Mug".to_string(), + price: 15.99, + in_stock: true, + }; + + let json_string = serde_json::to_string(&product).unwrap(); + println!("Product as JSON: {}", json_string); +} +``` + +--- + +**End of Lesson 6.** You've just gained powerful capabilities\! You now understand the basics of HTTP communication, the crucial role of asynchronous programming in Rust UIs, and how to fetch and process real-world data from APIs using `reqwest` and `serde`. This sets the stage for building truly dynamic and data-driven desktop applications. diff --git a/docs/desktop_applications/lesson7.md b/docs/desktop_applications/lesson7.md new file mode 100644 index 0000000..1dfd0ac --- /dev/null +++ b/docs/desktop_applications/lesson7.md @@ -0,0 +1,567 @@ +--- +title: API Integration & UI Updates +sidebar_position: 7 +--- + +Okay, understood\! Let's simplify Module 7 to make it even more approachable for beginners, focusing on the core concepts of API calls and UI updates without getting bogged down in too much data complexity or advanced Rust features. We'll stick to essential country info and keep the code as straightforward as possible. + +--- + +# Lesson 7: API Integration & UI Updates + +Fantastic\! You've learned how Rust handles asynchronous operations and how to talk to APIs. Now, it's time to bring that power into your desktop applications\! In this module, we'll integrate our API communication skills with Slint, making our UIs dynamic and data-driven. Get ready to fetch real-world data and display it beautifully\! 🌍 + +--- + +### 7.1 Practical Exercises: Consuming External APIs and Parsing Responses (Rest Countries API) + +Our main goal here is to fetch data from the **Rest Countries API** and display it in our Slint application. This will involve: + +1. Defining simple Rust `struct`s to match the API's JSON response for the data we care about. +2. Making `async` HTTP `GET` requests using `reqwest`. +3. Deserializing the JSON response into our Rust `struct`s using `serde`. +4. **Crucially:** Handling the asynchronous nature of the API call so that our Slint UI remains responsive. + +#### **Exercise 1: Displaying a Basic List of All Countries** + +Let's start by fetching a list of all countries and displaying their names in a scrollable list within our Slint app. + +**Preparation:** + +1. **Create a new Cargo project:** `cargo new slint_countries_app` + +2. **Update `Cargo.toml`:** Add `slint`, `slint-build`, `tokio`, `reqwest`, `serde`, and `serde_json` dependencies. + + ```toml + # Cargo.toml + [package] + name = "slint_countries_app" + version = "0.1.0" + edition = "2021" + + [dependencies] + slint = "1.x" + tokio = { version = "1", features = ["full"] } # For async runtime + reqwest = { version = "0.11", features = ["json"] } # For HTTP requests + serde = { version = "1.0", features = ["derive"] } # For JSON serialization/deserialization + serde_json = "1.0" + + [build-dependencies] + slint-build = "1.x" + ``` + +3. **Define Simple Rust Structs for Country Data:** + We'll focus on just a few key pieces of information from the Rest Countries API (`https://restcountries.com/v3.1/all`) to keep our structs straightforward. + + ```rust + // src/main.rs (add these struct definitions at the top, before main) + use serde::Deserialize; // Make sure this is imported + use std::collections::HashMap; // Needed for the 'languages' field + + #[derive(Debug, Deserialize, Clone)] // Clone is useful for passing data around + pub struct Country { + pub name: CountryName, + pub population: u64, + pub region: String, + #[serde(default)] // Use default if 'capital' array is missing + pub capital: Vec, // Capital is an array of strings + pub languages: Option>, // Languages is a map, can be optional + pub cca3: String, // Country code, useful for detail lookup + } + + #[derive(Debug, Deserialize, Clone)] + pub struct CountryName { + pub common: String, + pub official: String, + } + ``` + +**UI Design (`ui.slint`):** + +We'll use a `ListView` to display the country names. `ListView` works by taking a `model` property, which expects a list of items. Slint can automatically generate a model from a `Vec` in Rust if `T` implements `Default` and `Clone`. + +```slint +// ui.slint +import { ListView, VerticalBox, Text, LineEdit, StandardListView } from "std-widgets.slint"; + +// Define a Slint struct that mirrors the essential parts of our Rust Country struct +// This is what the ListView will display. +component CountryListItem { + in property name; + in property code; // To identify the country when clicked later + + Rectangle { + width: 100%; + height: 40px; + background: #CFD8DC; // Light grey background + border-radius: 5px; + padding: 10px; + + Text { + text: name; + vertical-alignment: center; + horizontal-alignment: left; + font-size: 16px; + color: #263238; + } + + // We'll add click handling in the next exercise + } +} + +export component MainWindow inherits Window { + width: 800px; + height: 600px; + title: "Global Countries Dashboard"; + + // This property will hold our list of countries + // It's an 'in' property because Rust will provide the data. + in property <[CountryListItem]> countries_model: []; + + VerticalBox { + alignment: center; + spacing: 10px; + padding: 15px; + + Text { + text: "All Countries"; + font-size: 32px; + font-weight: 700; + color: #0D47A1; // Dark blue + horizontal-alignment: center; + } + + // Search bar (static for now) + LineEdit { + placeholder-text: "Search for a country..."; + width: 90%; + height: 40px; + font-size: 16px; + } + + // The ListView to display our countries + ListView { + width: 90%; + height: 100%; // Take available vertical space + model: countries_model; // Bind to our property + // Delegate defines how each item in the list looks + delegate: CountryListItem { + name: root.name; // Bind delegate's name to model item's name + code: root.code; // Bind delegate's code to model item's code + } + } + } +} +``` + +**Rust Logic (`src/main.rs`):** + +Now, let's put it all together. We'll make the API call in an `async` task and then update the Slint UI. + +```rust +// src/main.rs +slint::include_modules!(); // This compiles ui.slint and generates Rust code + +use tokio; // For async runtime +use reqwest; // For HTTP requests +use serde::Deserialize; // For JSON deserialization +use slint::VecModel; // For managing lists in Slint +use std::rc::Rc; // For shared ownership (needed for VecModel) +use std::collections::HashMap; // For the languages map + +// --- Structs for API Response (defined above in the lesson content) --- +// #[derive(Debug, Deserialize, Clone)] +// pub struct Country { ... } +// #[derive(Debug, Deserialize, Clone)] +// pub struct CountryName { ... } + +#[tokio::main] +async fn main() -> Result<(), Box> { + let ui = MainWindow::new().unwrap(); + let ui_handle_weak = ui.as_weak(); // Get a weak handle for use in closures + + // --- Spawn an async task to fetch data --- + tokio::spawn(async move { + let api_url = "https://restcountries.com/v3.1/all"; + println!("Fetching all countries from API..."); + + let client = reqwest::Client::new(); + let response = client.get(api_url).send().await; + + match response { + Ok(res) => { + if res.status().is_success() { + let countries_data: Result, _> = res.json().await; + match countries_data { + Ok(mut fetched_countries) => { + // Sort countries alphabetically by common name + fetched_countries.sort_by(|a, b| a.name.common.cmp(&b.name.common)); + + // Prepare data for Slint's model + let slint_items: Vec = fetched_countries.into_iter().map(|c| { + CountryListItem { + name: c.name.common.into(), // Convert String to slint::SharedString + code: c.cca3.into(), // Convert String to slint::SharedString + } + }).collect(); + + // Update the UI on the Slint event loop + if let Some(ui) = ui_handle_weak.upgrade() { + // Use Rc::new(VecModel::from(...)) to create a model from our Vec + ui.set_countries_model(Rc::new(VecModel::from(slint_items))); + println!("Successfully loaded {} countries into UI.", ui.get_countries_model().row_count()); + } + }, + Err(e) => { + eprintln!("Failed to deserialize countries data: {:?}", e); + if let Some(ui) = ui_handle_weak.upgrade() { + ui.set_countries_model(Rc::new(VecModel::from(vec![ + CountryListItem { name: "Error loading data!".into(), code: "".into() } + ]))); + } + } + } + } else { + eprintln!("API returned an error status: {}", res.status()); + if let Some(ui) = ui_handle_weak.upgrade() { + ui.set_countries_model(Rc::new(VecModel::from(vec![ + CountryListItem { name: format!("API Error: {}", res.status()).into(), code: "".into() } + ]))); + } + } + }, + Err(e) => { + eprintln!("Network request failed: {:?}", e); + if let Some(ui) = ui_handle_weak.upgrade() { + ui.set_countries_model(Rc::new(VecModel::from(vec![ + CountryListItem { name: format!("Network Error: {:?}", e).into(), code: "".into() } + ]))); + } + } + } + }); + + ui.run().unwrap(); // Run the Slint UI event loop + Ok(()) +} +``` + +--- + +### 7.2 Dynamically Updating Slint UI Elements with Collected Data + +Now that we can display a list, let's make it interactive. When a user clicks on a country in the `ListView`, we want to fetch its detailed information and display it in a separate panel. + +#### **Exercise 2: Displaying Country Details on Click** + +**UI Design (`ui.slint` - Modifications):** + +1. Add a `callback` to `CountryListItem` so it can signal when it's clicked. +2. Add a new section (e.g., a `VerticalBox` or `GridBox`) to `MainWindow` to display country details. +3. Add properties to `MainWindow` to hold the currently selected country's details. + + + +```slint +// ui.slint (MODIFIED) +import { ListView, VerticalBox, Text, LineEdit, StandardListView, GridBox, HorizontalBox } from "std-widgets.slint"; + +// MODIFIED: Add a callback to signal when an item is clicked +component CountryListItem { + in property name; + in property code; + callback clicked(string code); // New callback + + Rectangle { + width: 100%; + height: 40px; + background: #CFD8DC; + border-radius: 5px; + padding: 10px; + + Text { + text: name; + vertical-alignment: center; + horizontal-alignment: left; + font-size: 16px; + color: #263238; + } + + // Add a TouchArea to make the entire item clickable + TouchArea { + clicked => { + root.clicked(code); // When clicked, emit our callback with the country code + } + } + } +} + +export component MainWindow inherits Window { + width: 1000px; // Wider window to accommodate details + height: 600px; + title: "Global Countries Dashboard"; + + in property <[CountryListItem]> countries_model: []; + + // NEW PROPERTIES to display selected country details + in property selected_country_name: "Select a Country"; + in property selected_country_population: ""; + in property selected_country_region: ""; + in property selected_country_capital: ""; + in property selected_country_languages: ""; + + // NEW CALLBACK: When a country list item is clicked + callback request_country_details(string code); + + HorizontalBox { // Use HorizontalBox to split into list and details + spacing: 10px; + padding: 15px; + + VerticalBox { // Left side: Search and List + width: 300px; // Fixed width for the list panel + alignment: center; + spacing: 10px; + + Text { + text: "All Countries"; + font-size: 32px; + font-weight: 700; + color: #0D47A1; + horizontal-alignment: center; + } + + LineEdit { + placeholder-text: "Search for a country..."; + width: 100%; + height: 40px; + font-size: 16px; + // We'll add search logic in a later module! + } + + ListView { + width: 100%; + height: 100%; + model: countries_model; + delegate: CountryListItem { + name: root.name; + code: root.code; + // When a list item is clicked, trigger the MainWindow's callback + clicked => { root.request_country_details(root.code); } + } + } + } + + VerticalBox { // Right side: Country Details + width: 1fr; // Take remaining space + alignment: center; + spacing: 15px; + padding: 20px; + background: #E3F2FD; // Light blue background for details panel + border-radius: 8px; + + Text { + text: selected_country_name; + font-size: 36px; + font-weight: 700; + color: #1A237E; // Darker blue + horizontal-alignment: center; + wrap: word-wrap; + } + + GridBox { // Use a grid for key-value pairs + columns: 2; + rows: 4; // Reduced rows + column-spacing: 10px; + row-spacing: 5px; + width: 100%; + + Text { text: "Population:"; font-weight: 600; } Text { text: selected_country_population; } + Text { text: "Region:"; font-weight: 600; } Text { text: selected_country_region; } + Text { text: "Capital:"; font-weight: 600; } Text { text: selected_country_capital; } + Text { text: "Languages:"; font-weight: 600; } Text { text: selected_country_languages; wrap: word-wrap; } + } + } + } +} +``` + +**Rust Logic (`src/main.rs` - MODIFIED):** + +We'll add the `on_request_country_details` callback and the logic to fetch and display the details. We'll simplify the language and capital processing. + +```rust +// src/main.rs (MODIFIED) +slint::include_modules!(); + +use tokio; +use reqwest; +use serde::Deserialize; +use slint::{VecModel, SharedString}; // No Image, ImageBuffer needed +use std::rc::Rc; +use std::collections::HashMap; // For languages map + +// --- Structs for API Response (defined previously) --- +#[derive(Debug, Deserialize, Clone)] +pub struct Country { + pub name: CountryName, + pub population: u64, + pub region: String, + #[serde(default)] + pub capital: Vec, // Capital is an array of strings + pub languages: Option>, // Languages is a map, can be optional + pub cca3: String, // Country code, useful for detail lookup +} + +#[derive(Debug, Deserialize, Clone)] +pub struct CountryName { + pub common: String, + pub official: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let ui = MainWindow::new().unwrap(); + let ui_handle_weak = ui.as_weak(); // Weak handle for initial country list fetch + + // --- Initial fetch for all countries (same as before) --- + let ui_handle_clone_for_all_countries = ui_handle_weak.clone(); + tokio::spawn(async move { + let api_url = "https://restcountries.com/v3.1/all"; + println!("Fetching all countries from API..."); + + let client = reqwest::Client::new(); + let response = client.get(api_url).send().await; + + match response { + Ok(res) => { + if res.status().is_success() { + let countries_data: Result, _> = res.json().await; + match countries_data { + Ok(mut fetched_countries) => { + fetched_countries.sort_by(|a, b| a.name.common.cmp(&b.name.common)); + let slint_items: Vec = fetched_countries.into_iter().map(|c| { + CountryListItem { + name: c.name.common.into(), // Convert String to slint::SharedString + code: c.cca3.into(), // Convert String to slint::SharedString + } + }).collect(); + + if let Some(ui) = ui_handle_clone_for_all_countries.upgrade() { + ui.set_countries_model(Rc::new(VecModel::from(slint_items))); + println!("Successfully loaded {} countries into UI.", ui.get_countries_model().row_count()); + } + }, + Err(e) => eprintln!("Failed to deserialize countries data: {:?}", e), + } + } else { + eprintln!("API returned an error status: {}", res.status()); + } + }, + Err(e) => eprintln!("Network request failed: {:?}", e), + } + }); + + // --- NEW: Handle request for specific country details --- + let ui_handle_for_details = ui.as_weak(); // Another weak handle for the details callback + ui.on_request_country_details(move |code| { + let ui_handle_clone = ui_handle_for_details.clone(); // Clone for use inside the spawned task + let country_code = code.to_string(); // Convert SharedString to String + + tokio::spawn(async move { + let api_url = format!("https://restcountries.com/v3.1/alpha/{}", country_code); + println!("Fetching details for country code: {}", country_code); + + let client = reqwest::Client::new(); + let response = client.get(&api_url).send().await; + + match response { + Ok(res) => { + if res.status().is_success() { + let countries_data: Result, _> = res.json().await; + match countries_data { + Ok(fetched_countries) => { + if let Some(country) = fetched_countries.into_iter().next() { + // Use invoke_from_event_loop (implicitly handled by Slint when setting properties) + if let Some(ui) = ui_handle_clone.upgrade() { + // Simplify languages: get values, convert to Vec, join + let languages_str = country.languages + .map(|langs_map| { + langs_map.values() + .map(|s| s.to_string()) + .collect::>() + .join(", ") // Join them with a comma and space + }) + .unwrap_or_else(|| "N/A".to_string()); // Default if no languages + + // Simplify capital: take the first capital or "N/A" + let capital_str = country.capital.get(0) + .map(|s| s.to_string()) + .unwrap_or_else(|| "N/A".to_string()); + + + ui.set_selected_country_name(country.name.common.into()); + ui.set_selected_country_population(format!("{}", country.population).into()); + ui.set_selected_country_region(country.region.into()); + ui.set_selected_country_capital(capital_str.into()); + ui.set_selected_country_languages(languages_str.into()); + ui.set_selected_country_code(country.cca3.into()); // Also display code + } + } else { + eprintln!("No country data found in details response."); + if let Some(ui) = ui_handle_clone.upgrade() { + ui.set_selected_country_name("Details Not Found".into()); + ui.set_selected_country_population("".into()); + ui.set_selected_country_region("".into()); + ui.set_selected_country_capital("".into()); + ui.set_selected_country_languages("".into()); + ui.set_selected_country_code("".into()); + } + } + }, + Err(e) => { + eprintln!("Failed to deserialize country details: {:?}", e); + if let Some(ui) = ui_handle_clone.upgrade() { + ui.set_selected_country_name(format!("Error: {:?}", e).into()); + ui.set_selected_country_population("".into()); + ui.set_selected_country_region("".into()); + ui.set_selected_country_capital("".into()); + ui.set_selected_country_languages("".into()); + ui.set_selected_country_code("".into()); + } + } + } + } else { + eprintln!("API returned an error status for details: {}", res.status()); + if let Some(ui) = ui_handle_clone.upgrade() { + ui.set_selected_country_name(format!("API Error: {}", res.status()).into()); + ui.set_selected_country_population("".into()); + ui.set_selected_country_region("".into()); + ui.set_selected_country_capital("".into()); + ui.set_selected_country_languages("".into()); + ui.set_selected_country_code("".into()); + } + } + }, + Err(e) => { + eprintln!("Network request for details failed: {:?}", e); + if let Some(ui) = ui_handle_clone.upgrade() { + ui.set_selected_country_name(format!("Network Error: {:?}", e).into()); + ui.set_selected_country_population("".into()); + ui.set_selected_country_region("".into()); + ui.set_selected_country_capital("".into()); + ui.set_selected_country_languages("".into()); + ui.set_selected_country_code("".into()); + } + } + } + }); + }); + + ui.run().unwrap(); + Ok(()) +} +``` + +--- + +**End of Lesson 7.** You've successfully integrated external API data into your Slint UI\! You can now fetch lists of data, handle user clicks to get more details, and dynamically update your application's interface. This is a huge step towards building truly functional desktop applications. Next, we'll learn how to make your UIs more modular and reusable\! diff --git a/docs/desktop_applications/lesson8.md b/docs/desktop_applications/lesson8.md new file mode 100644 index 0000000..5812c3d --- /dev/null +++ b/docs/desktop_applications/lesson8.md @@ -0,0 +1,355 @@ +--- +title: Reusability & Customization +sidebar_position: 8 +--- + +--- + +# Lesson 8: Advanced Slint Features - Reusability & Customization + +Welcome back\! So far, you've built interactive UIs and even connected them to external APIs. That's awesome\! But as your applications grow, you'll want to keep your UI code clean, organized, and easy to manage. That's where **UI components** and **custom styling** come in. In this module, we'll learn how to build reusable UI blocks and make them look exactly how you want. ✨ + +--- + +### 8.1 Understanding the Power of UI Components (5 min) + +Think of UI components like LEGO bricks. Instead of building everything from scratch every time, you create smaller, self-contained, and specialized pieces that you can then snap together to build larger, more complex structures. + +- A **UI Component** is a self-contained, independent, and reusable block of UI. It encapsulates its own layout, appearance, and sometimes its own internal behavior. +- Examples: A button, a text input field, a navigation bar, a user profile card, or even a complex chart can all be components. +- In Slint, everything is essentially a component, from the top-level `Window` to a simple `Text` element. When you define your own, you're just creating custom versions of these building blocks. + +--- + +### 8.2 Benefits of Component Reusability in UI Development (5 min) + +Why bother breaking your UI into components? It's a game-changer for development efficiency and quality\! + +1. **Consistency:** Using the same component across your app ensures a consistent look and feel. No more slightly different buttons on different screens\! +2. **Maintainability:** If you need to change how a button looks or behaves, you only change it in one place (the component's definition), and all instances of that button update automatically. This saves a ton of time and reduces bugs. +3. **Faster Development:** Once a component is built, you can reuse it anywhere. This speeds up the development process significantly, as you're not constantly rewriting the same UI patterns. +4. **Easier Debugging:** If there's a bug in a specific UI element, you know exactly where to look – within that component's code. +5. **Collaboration:** Teams can work on different components simultaneously, speeding up overall project delivery. + +--- + +### 8.3 Crafting Your First Reusable Slint Component (20 min) + +Let's create a custom button component that we can reuse throughout our application. It will have a custom background color and text color. + +1. **Open your `slint_countries_app` project (or create a new one).** +2. **Open `ui.slint`.** + +Now, define a new `component` named `CustomButton`. This component will `inherit` from `Rectangle` (to give it a background and dimensions) and will have properties for its text and colors. + +```slint +// ui.slint (add this new component definition, perhaps above MainWindow) +import { Text, Rectangle, TouchArea } from "std-widgets.slint"; // Ensure TouchArea is imported + +export component CustomButton inherits Rectangle { + // Input properties for our custom button + in property text_content; + in property button_background_color: #424242; // Default dark grey + in property text_color: white; // Default white text + + // Output callback for when the button is clicked + callback clicked(); + + // The visual elements that make up our button + Rectangle { + // This rectangle forms the main background of the button + width: 100%; // Fill the parent CustomButton's width + height: 100%; // Fill the parent CustomButton's height + background: button_background_color; // Use the input property for background + border-radius: 8px; // Rounded corners + } + + Text { + // This text element displays the button's label + text: text_content; // Use the input property for text + color: text_color; // Use the input property for text color + font-size: 18px; + horizontal-alignment: center; + vertical-alignment: center; + } + + // A TouchArea makes the entire component clickable + TouchArea { + width: 100%; + height: 100%; + clicked => { + root.clicked(); // When this TouchArea is clicked, emit our CustomButton's 'clicked' callback + } + } +} + +// --- Now, use this CustomButton in your MainWindow (or a new test component) --- +// Example usage in MainWindow (replace an existing button or add a new one) +export component MainWindow inherits Window { + // ... (existing properties and layout) ... + + VerticalBox { + // ... (existing elements) ... + + // Using our new CustomButton! + CustomButton { + text_content: "Click Me!"; + button_background_color: #FF5722; // Orange + text_color: white; + width: 150px; + height: 50px; + clicked => { + // This will be handled in Rust, just like other callbacks + debug("Custom button clicked!"); + } + } + + CustomButton { + text_content: "Another Button"; + button_background_color: #607D8B; // Blue Grey + text_color: white; + width: 180px; + height: 50px; + clicked => { + debug("Another custom button clicked!"); + } + } + + // ... (rest of your MainWindow) ... + } +} +``` + +**Explanation:** + +- `export component CustomButton inherits Rectangle`: We define a new reusable component. It `inherits Rectangle` because we want it to behave like a rectangle (have `width`, `height`, `background`, etc.) and be a visual container. +- `in property text_content;`: This declares an "input" property. When you use `CustomButton`, you'll set `text_content` like `CustomButton { text_content: "My Label"; }`. +- `button_background_color: #424242;`: We set default values for properties. +- `text: text_content;`: This is **data binding**\! The `Text` element's `text` property is bound to our component's `text_content` property. +- `callback clicked();`: This declares an "output" callback. The `TouchArea` inside the button will trigger `root.clicked();` which then triggers this component's `clicked` callback. You'll handle this in Rust using `my_button.on_clicked(|| { ... });`. + +**Run `cargo run`** to see your custom buttons\! + +--- + +### 8.4 Customizing UI Aesthetics: Creating Bespoke Styles (20 min) + +Slint gives you powerful ways to control the look and feel of your UI. You can apply styles directly to elements, or define reusable styles and color palettes. + +#### **Direct Styling:** + +You've already done this\! Setting properties like `background: blue;` or `font-size: 24px;` directly on an element is the simplest form of styling. + +#### **Using `brushes` for Reusable Colors/Gradients:** + +Instead of hardcoding color values everywhere, you can define named `brushes` (which can be solid colors, gradients, or images) and reuse them. + +```slint +// ui.slint (add this at the top, perhaps after imports) + +// Define some reusable colors/brushes +global MyPalette { + // Solid colors + brush primary_color: #1976D2; // Deep Blue + brush accent_color: #FFC107; // Amber + brush text_dark: #212121; // Dark Grey + brush text_light: white; + + // A simple linear gradient + brush gradient_bg: linear-gradient(0deg, #42A5F5, #1976D2); // Light blue to deep blue +} + +export component MainWindow inherits Window { + // ... + VerticalBox { + background: MyPalette.gradient_bg; // Use the defined gradient brush + // ... + Text { + text: "Welcome!"; + color: MyPalette.text_light; // Use the defined text color + font-size: 30px; + } + + CustomButton { + text_content: "Styled Button"; + button_background_color: MyPalette.accent_color; // Use defined accent color + text_color: MyPalette.text_dark; + width: 200px; height: 50px; + clicked => { debug("Styled button clicked!"); } + } + } +} +``` + +- `global MyPalette { ... }`: Defines a global scope where you can put reusable definitions. +- `brush primary_color: #1976D2;`: Declares a named brush. +- `background: MyPalette.gradient_bg;`: Accesses the brush using its global name. + +#### **Widget Styles and Platform Adaptation:** + +Slint widgets (`Button`, `LineEdit` etc.) automatically adapt to the native look and feel of the operating system (e.g., Fluent on Windows, Cupertino on macOS). You can also explicitly choose a style or override parts of it. + +- **Default Behavior:** Slint tries to use a "native" style. +- **Overriding (via environment variable):** You can force a style (e.g., `SLINT_STYLE=material cargo run`) for testing. +- **Customizing within `ui.slint`:** You can override default styles for specific widgets or create your own custom styles. + +**Example: Overriding a default button style:** + +```slint +// ui.slint +import { Button } from "std-widgets.slint"; + +// You can create a component that just sets default styles for a standard widget +export component MyStyledButton inherits Button { + background: #8BC34A; // Lime green background + border-radius: 10px; + color: white; + font-size: 18px; + height: 45px; +} + +export component MainWindow inherits Window { + // ... + VerticalBox { + // ... + MyStyledButton { + text: "Custom Look"; + clicked => { debug("Custom look button clicked!"); } + } + // ... + } +} +``` + +This allows you to wrap standard widgets with your own default styles, promoting consistency. + +--- + +### 8.5 Practical Application Exercise (40 min) + +It's time to put your component and styling skills to the test\! + +**Exercise: Create a "User Profile Card" component.** + +This component will display a user's name, email, and a status (e.g., "Online" or "Offline"). It should be reusable and styled. + +- **Requirements for `UserProfileCard` component:** + + - It should be an `export component` so you can use it in `MainWindow`. + - It should take `in property` for: + - `user_name` (string) + - `user_email` (string) + - `is_online` (boolean) + - **Layout:** Use a `HorizontalBox` or `VerticalBox` to arrange the elements neatly. + - **Styling:** + - Give the entire card a background color and `border-radius`. + - Style the `user_name` with a larger, bolder font. + - Style the `user_email` with a smaller, lighter font. + - Display the `is_online` status using a `Text` element. + - **Conditional Styling:** If `is_online` is `true`, make the status text green. If `false`, make it red. (Hint: use `if` expressions in properties, e.g., `color: is_online ? green : red;`) + - **Optional:** Add a placeholder `Image` element for a profile picture. + +- **Usage in `MainWindow`:** + + - In your `MainWindow`, create at least two instances of your `UserProfileCard` component, passing different data to each to show its reusability. + +**Example `ui.slint` structure (Don't copy-paste, build it\!):** + +```slint +// ui.slint (for User Profile Card) +import { Text, Rectangle, VerticalBox, HorizontalBox, Image } from "std-widgets.slint"; + +// Define reusable colors (optional, but good practice) +global AppColors { + brush card_bg: #F5F5F5; // Light grey + brush online_color: #4CAF50; // Green + brush offline_color: #F44336; // Red + brush text_primary: #212121; + brush text_secondary: #757575; +} + +export component UserProfileCard inherits Rectangle { + width: 300px; + height: 120px; + background: AppColors.card_bg; + border-radius: 10px; + padding: 15px; + + in property user_name; + in property user_email; + in property is_online; + + HorizontalBox { + spacing: 10px; + alignment: center; + + Image { + source: "assets/profile_placeholder.png"; // Placeholder image + width: 80px; + height: 80px; + border-radius: 40px; // Make it circular + } + + VerticalBox { + spacing: 5px; + alignment: start; // Align text to the left + + Text { + text: user_name; + font-size: 20px; + font-weight: 700; + color: AppColors.text_primary; + } + + Text { + text: user_email; + font-size: 14px; + color: AppColors.text_secondary; + } + + Text { + text: is_online ? "Online" : "Offline"; // Conditional text + font-size: 14px; + font-weight: 500; + color: is_online ? AppColors.online_color : AppColors.offline_color; // Conditional color + } + } + } +} + +export component MainWindow inherits Window { + width: 700px; + height: 400px; + title: "Reusable Components Demo"; + + VerticalBox { + alignment: center; + spacing: 20px; + padding: 30px; + + Text { + text: "Our Team"; + font-size: 40px; + font-weight: 700; + color: #4CAF50; + horizontal-alignment: center; + } + + UserProfileCard { + user_name: "Alice Smith"; + user_email: "alice@example.com"; + is_online: true; + } + + UserProfileCard { + user_name: "Bob Johnson"; + user_email: "bob@example.com"; + is_online: false; + } + } +} +``` + +--- + +**End of Lesson 8.** You've now mastered the art of creating reusable UI components and applying custom styles in Slint\! This will significantly boost your productivity and help you build cleaner, more maintainable, and visually consistent desktop applications. Next, we'll dive into local data persistence\! diff --git a/docs/desktop_applications/lesson9.md b/docs/desktop_applications/lesson9.md new file mode 100644 index 0000000..5d35670 --- /dev/null +++ b/docs/desktop_applications/lesson9.md @@ -0,0 +1,419 @@ +--- +title: Local Storage & File System +sidebar_position: 9 +--- + +--- + +# Lesson 9: Data Persistence - Local Storage & File System + +So far, our apps lose all their data when you close them. This isn't very practical for things like user settings, saved progress, or stored notes. Today, we'll learn how to make your Rust desktop applications **remember** data, even after they've been closed and reopened. This is called **data persistence**. 💾 + +--- + +### 9.1 The Necessity of Persistent Data in Desktop Applications (5 min) + +Imagine your favorite word processor. What if every time you closed it, your document disappeared? Or your game, where your progress vanished? That would be incredibly frustrating\! + +- **User Experience:** Users expect applications to remember their preferences, last-opened files, login status, and any data they've created. +- **Application State:** Many applications need to store internal data that defines their current state or configuration. +- **Offline Capability:** Desktop applications often need to work without an internet connection, requiring local storage of data. +- **Data Integrity:** Ensuring that data is saved reliably and can be retrieved later. + +Without data persistence, your applications are essentially stateless, starting fresh every time they run. + +--- + +### 9.2 Overview of Local Data Persistence Strategies (5 min) + +When we talk about storing data locally on a user's computer, there are several common approaches, each with its own pros and cons: + +1. **Configuration Files (e.g., INI, TOML, YAML, JSON):** + + - **Concept:** Store simple settings or small amounts of structured data in plain text files. + - **Pros:** Easy to read/edit manually, simple to implement for basic needs. + - **Cons:** Not suitable for large amounts of data, complex queries, or concurrent access. + +2. **Plain Text Files / Custom Formats:** + + - **Concept:** Save raw data directly to files in a format you define. + - **Pros:** Full control over format. + - **Cons:** Requires manual parsing, error-prone, no built-in querying. + +3. **Embedded Databases (e.g., SQLite, RocksDB):** + + - **Concept:** A full-fledged database system that runs directly within your application process, storing data in a single file. + - **Pros:** Robust, supports complex queries (SQL), handles large amounts of data, ensures data integrity, can manage concurrent access (within limits). + - **Cons:** More complex setup than plain files, adds a dependency to your application. + +4. **Platform-Specific Storage:** + + - **Concept:** Using OS-provided mechanisms (e.g., Windows Registry, macOS `UserDefaults`). + - **Pros:** Integrates well with the OS. + - **Cons:** Not cross-platform, often limited in data types or size. + +For this module, we'll focus on **Direct File System Interactions** for simpler needs and then dive into **Embedded Databases** (specifically SQLite) for more robust and structured data persistence, as it's a very common and powerful solution for desktop apps. + +--- + +### 9.3 Direct File System Interactions (20 min) + +Rust's standard library provides robust tools for interacting with the file system. You can read from and write to files directly. This is great for configuration files, log files, or small, unstructured data. + +#### **Reading from a File:** + +To read a file, you'll typically use `std::fs::File::open()` to get a `File` handle, then `std::io::Read` methods (like `read_to_string()`). Remember, file operations return `Result` because they can fail\! + +```rust +use std::fs; // For file system operations +use std::io::{self, Read, Write}; // For I/O traits + +fn main() -> Result<(), Box> { + let file_path = "data/config.txt"; // Relative path to a file + + // Create a 'data' directory if it doesn't exist + fs::create_dir_all("data")?; + + // Write some initial content to the file if it doesn't exist + if !std::path::Path::new(file_path).exists() { + let mut file = fs::File::create(file_path)?; + file.write_all(b"app_version=1.0\nusername=default_user")?; + println!("Created initial config file: {}", file_path); + } + + println!("\n--- Reading from {} ---", file_path); + let mut file = fs::File::open(file_path)?; // Open the file. Returns Result + let mut contents = String::new(); + file.read_to_string(&mut contents)?; // Read contents into a String. Returns Result + + println!("File contents:\n{}", contents); + + Ok(()) +} +``` + +- **`std::fs`:** The module for file system operations. +- **`File::open()`:** Tries to open a file. Returns `Ok(File)` or `Err(io::Error)`. +- **`read_to_string()`:** Reads the entire file into a `String`. +- **`create_dir_all()`:** Creates a directory and any necessary parent directories. +- **`write_all()`:** Writes bytes to a file. + +#### **Writing to a File:** + +To write to a file, you'll use `std::fs::File::create()` (which creates a new file or truncates an existing one) or `std::fs::OpenOptions` for more control (e.g., appending). Then use `std::io::Write` methods. + +```rust +use std::fs; +use std::io::{self, Write}; + +fn main() -> Result<(), Box> { + let file_path = "data/log.txt"; + + println!("\n--- Writing to {} ---", file_path); + + // Create a file (or overwrite if it exists) + let mut file = fs::File::create(file_path)?; + file.write_all(b"Application started.\n")?; + println!("Wrote 'Application started.'"); + + // Open in append mode to add more content without overwriting + let mut file = fs::OpenOptions::new() + .append(true) // Open for appending + .open(file_path)?; // Open the file + + file.write_all(b"User logged in.\n")?; + println!("Appended 'User logged in.'"); + + Ok(()) +} +``` + +- **`File::create()`:** Creates a new file. If a file with the same name already exists, it will be truncated (emptied) or overwritten. +- **`OpenOptions::new().append(true).open()`:** This is how you open a file specifically to _append_ data to its end, without deleting existing content. + +#### **Working with Paths:** + +The `std::path` module helps you work with file paths in a cross-platform way. + +```rust +use std::path::Path; + +fn main() { + let path = Path::new("data/config.txt"); + + println!("Path exists: {}", path.exists()); + println!("File name: {:?}", path.file_name()); + println!("Parent directory: {:?}", path.parent()); + println!("Is absolute: {}", path.is_absolute()); +} +``` + +**Considerations for File I/O in Slint Apps:** + +- **Blocking:** `std::fs` operations are **synchronous (blocking)**. For simple config files on app startup/shutdown, this is usually fine. But for large files or frequent operations that might freeze your UI, you'd want to perform these in an `async` task using `tokio::fs` (which provides async versions of file operations). +- **Error Handling:** Always handle `Result` types for file operations. +- **Application Data Directories:** For real apps, you'd typically save user-specific data in OS-specific application data directories (e.g., `~/.config/my_app` on Linux, `~/Library/Application Support/my_app` on macOS, `%APPDATA%\my_app` on Windows). Crates like `dirs` can help find these paths. + +--- + +### 9.4 Achieving Robust Local Data Persistence: Integrating an Embedded Database (60 min) + +For structured data, large datasets, or when you need to perform queries, an **embedded database** is the way to go. **SQLite** is the most popular choice for desktop applications because it's a self-contained, serverless, zero-configuration, transactional SQL database engine. It stores the entire database in a single file on disk. + +We'll use the `rusqlite` crate, which is the official SQLite binding for Rust. + +#### **Setting Up `rusqlite`:** + +1. **Create a new Cargo project:** `cargo new slint_db_app` +2. **Update `Cargo.toml`:** Add `slint`, `slint-build`, and `rusqlite`. + + ```toml + # Cargo.toml + [package] + name = "slint_db_app" + version = "0.1.0" + edition = "2021" + + [dependencies] + slint = "1.x" + rusqlite = { version = "0.31", features = ["bundled"] } # "bundled" includes SQLite library, no system install needed + + [build-dependencies] + slint-build = "1.x" + ``` + + - `features = ["bundled"]` for `rusqlite` is very convenient as it includes the SQLite C library with your app, so users don't need to have SQLite installed on their system. + +#### **Core SQLite Operations with `rusqlite`:** + +Let's create a simple Slint app that manages a list of "tasks" stored in an SQLite database. + +**1. Database Connection and Table Creation:** + +```rust +// src/main.rs (initial setup for DB) +use rusqlite::{Connection, Result}; // Import Connection and Result from rusqlite + +fn setup_database() -> Result { + // Open a connection to a SQLite database file. + // If the file doesn't exist, it will be created. + let conn = Connection::open("tasks.db")?; + + // Create the 'tasks' table if it doesn't already exist. + // This SQL statement defines the table schema. + conn.execute( + "CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + description TEXT NOT NULL, + completed INTEGER NOT NULL DEFAULT 0 + )", + [], // No parameters for this query + )?; + + Ok(conn) // Return the database connection +} + +fn main() -> Result<(), Box> { + let conn = setup_database()?; // Call our setup function + + println!("Database 'tasks.db' opened and table 'tasks' ensured."); + + // You can now use 'conn' for further database operations. + // For now, we'll just keep the app running briefly. + // In a real Slint app, you'd pass this connection or a reference to it + // to your UI logic. + + // A dummy loop to keep the console app alive for demonstration + // In a Slint app, ui.run() would keep it alive. + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + // conn will automatically close when it goes out of scope (program ends) +} +``` + +- **`Connection::open("tasks.db")`:** Connects to the database file. +- **`conn.execute()`:** Used for SQL commands that don't return rows (like `CREATE TABLE`, `INSERT`, `UPDATE`, `DELETE`). The `[]` is for query parameters. + +**2. Inserting Data:** + +```rust +// src/main.rs (add this function) +fn add_task(conn: &Connection, description: &str) -> Result<()> { + conn.execute( + "INSERT INTO tasks (description) VALUES (?1)", // ?1 is a placeholder for the first parameter + [description], // Pass the description as a slice of references + )?; + println!("Task added: '{}'", description); + Ok(()) +} + +fn main() -> Result<(), Box> { + let conn = setup_database()?; + + add_task(&conn, "Learn Rust Ownership")?; + add_task(&conn, "Build Slint App")?; + add_task(&conn, "Integrate SQLite")?; + + // ... rest of main ... +} +``` + +- **`?1`:** A positional parameter placeholder. `rusqlite` also supports named parameters (`:name`). +- **`[description]`:** Parameters are passed as a slice. + +**3. Querying (Reading) Data:** + +To read data, you use `conn.prepare()` to create a `Statement`, then `query_map()` or `query_and_then()` to iterate over rows. You'll often map rows to Rust `struct`s. + +```rust +// src/main.rs (add this struct and function) +#[derive(Debug, Clone)] // Add Clone for Slint model later +struct Task { + id: i32, + description: String, + completed: bool, +} + +fn get_all_tasks(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT id, description, completed FROM tasks")?; + let task_iter = stmt.query_map([], |row| { + // This closure is called for each row + Ok(Task { + id: row.get(0)?, // Get value from column 0 + description: row.get(1)?, // Get value from column 1 + completed: row.get(2)?, // Get value from column 2 + }) + })?; + + let tasks: Result> = task_iter.collect(); // Collect all results into a Vec + tasks +} + +fn main() -> Result<(), Box> { + let conn = setup_database()?; + add_task(&conn, "Learn Rust Ownership")?; + add_task(&conn, "Build Slint App")?; + add_task(&conn, "Integrate SQLite")?; + + println!("\n--- All Tasks ---"); + let tasks = get_all_tasks(&conn)?; + for task in tasks { + println!("{:?}", task); + } + + // ... rest of main ... +} +``` + +- **`conn.prepare()`:** Creates a prepared statement, which is more efficient for repeated queries and prevents SQL injection. +- **`query_map()`:** Executes the query and maps each row to a Rust type using the provided closure. +- **`row.get(index)?`:** Retrieves a value from a column by its zero-based index. + +**4. Updating Data:** + +```rust +// src/main.rs (add this function) +fn mark_task_completed(conn: &Connection, task_id: i32) -> Result<()> { + conn.execute( + "UPDATE tasks SET completed = ?1 WHERE id = ?2", + [1, task_id], // 1 for true, task_id for the WHERE clause + )?; + println!("Task {} marked as completed.", task_id); + Ok(()) +} + +fn main() -> Result<(), Box> { + let conn = setup_database()?; + add_task(&conn, "Learn Rust Ownership")?; + add_task(&conn, "Build Slint App")?; + + mark_task_completed(&conn, 1)?; // Mark the first task as completed + + println!("\n--- Tasks after update ---"); + let tasks = get_all_tasks(&conn)?; + for task in tasks { + println!("{:?}", task); + } + + // ... rest of main ... +} +``` + +**5. Deleting Data:** + +```rust +// src/main.rs (add this function) +fn delete_task(conn: &Connection, task_id: i32) -> Result<()> { + conn.execute( + "DELETE FROM tasks WHERE id = ?1", + [task_id], + )?; + println!("Task {} deleted.", task_id); + Ok(()) +} + +fn main() -> Result<(), Box> { + let conn = setup_database()?; + add_task(&conn, "Learn Rust Ownership")?; + add_task(&conn, "Build Slint App")?; + + delete_task(&conn, 2)?; // Delete the task with ID 2 + + println!("\n--- Tasks after deletion ---"); + let tasks = get_all_tasks(&conn)?; + for task in tasks { + println!("{:?}", task); + } + + // ... rest of main ... +} +``` + +#### **Integrating with Slint (Conceptual):** + +In a Slint application, you would: + +1. **Initialize the database connection** (`setup_database()`) once, typically at the start of `main()`. +2. **Pass the `Connection` (or a shared reference to it, e.g., `Rc>` for mutable access across threads/closures, or `Arc>` for multi-threaded async access) to your Slint UI's callbacks.** +3. **In your Slint UI (`ui.slint`):** + - Define a `ListView` to display tasks. + - Define `in-out property <[TaskListItem]> tasks_model;` + - Add buttons for "Add Task," "Mark Completed," "Delete Task." + - Define callbacks (e.g., `callback add_task(string description);`). +4. **In your Rust `main.rs`:** + - Implement the callbacks (`on_add_task`, `on_mark_completed`, `on_delete_task`). + - Inside these callbacks, call your `rusqlite` functions (`add_task`, `mark_task_completed`, `delete_task`). + - **Crucially:** After any database modification, you'll need to **re-fetch the tasks** (`get_all_tasks()`) and **update the `tasks_model` property** in your Slint UI to reflect the changes. + - **Async Considerations:** Database operations can be blocking. For a responsive UI, you would typically run these `rusqlite` calls inside a `tokio::task::spawn_blocking` block if your main application is `async`, or manage them in a separate thread. + +**Example `slint::VecModel` for `Task` struct:** + +```rust +// Add this to your ui.slint +component TaskListItem { + in property description; + in property completed; + in property id; // Keep ID for updates/deletes + // Add callbacks for mark/delete buttons if you put them on each item +} + +export component MainWindow inherits Window { + // ... + in property <[TaskListItem]> tasks_model: []; + // ... + ListView { + model: tasks_model; + delegate: TaskListItem { + description: root.description; + completed: root.completed; + id: root.id; + // ... + } + } +} +``` + +--- + +**End of Lesson 9.** You've now gained essential skills for data persistence in desktop applications\! You can interact directly with the file system for simple data and, more importantly, integrate a robust embedded database like SQLite to manage structured data efficiently. This is a massive step towards building powerful, stateful applications. Next, we'll cover best practices and deployment\!