Rust-Werkstatt
Werkstatt · Fährmann · Etappe 1 von 13

Wir bauen einen Commander – das Fenster

Schön, dass du da bist! Hol dir einen Kaffee, rück deinen Stuhl heran. In dieser Werkstatt bauen wir zusammen ein echtes Programm: einen Dateimanager, mit dem du Dateien per FTP und SFTP zwischen deinem Rechner und einem Server hin- und herschiebst. Aussehen soll er wie der gute alte Norton Commander. Heute legen wir den Grundstein. Am Ende dieser Etappe hast du ein Fenster, das schon genau nach Commander aussieht.

8 Schritte · ca. 45 Minuten · iced 0.14 · Rust 1.88 oder neuer

Ein Fenster mit türkiser Menüzeile oben, großer blauer Fläche in der Mitte und einer Leiste mit den Funktionstasten 1 bis 10 unten
So sieht unser Programm am Ende dieser Etappe aus.
Vorab

Bevor wir loslegen

Was ist eigentlich der Norton Commander?

Der Norton Commander kam 1986 für DOS heraus, und wer in den 90ern an einem PC saß, kennt ihn: ein blauer Bildschirm, zwei Listen mit Dateien nebeneinander, und unten eine Leiste mit den Funktionstasten F1 bis F10. Links ein Ordner, rechts ein Ordner, und mit F5 kopierst du die markierte Datei von der einen Seite auf die andere. Das Prinzip war so gut, dass es bis heute weiterlebt, zum Beispiel im Midnight Commander unter Linux oder im Total Commander unter Windows.

Für unser Programm ist diese Idee perfekt: Links ist dein eigener Rechner, rechts der Server. Dateien hochladen heißt dann einfach: links markieren, F5 drücken. Deshalb heißt unser Programm auch Fährmann. Er bringt deine Dateien vom einen Ufer ans andere.

Was du brauchst

  • Rust, installiert über rustup. Falls du das noch nicht hast: Buch, Kapitel 1 zeigt dir Schritt für Schritt, wie es geht.
  • Einen Editor, am besten VS Code mit der Erweiterung rust-analyzer.
  • Ein Terminal, in dem du Befehle eintippst.

iced musst du noch nicht kennen. Ich erkläre alles, was wir brauchen, genau in dem Moment, in dem wir es brauchen. Wo es sich lohnt, verlinke ich auf das passende Kapitel im Kurs, falls du etwas genauer wissen willst.

So liest du die Code-Kästen

Wir schreiben nicht bei jedem Schritt das ganze Programm neu. Stattdessen zeige ich dir genau die Stelle, an der sich etwas ändert. Damit du sie in deiner Datei wiederfindest, stehen oben im Kasten immer der Dateiname und ein kurzer Hinweis, wo die Stelle liegt. Die Zeilen im Kasten sind farbig markiert:

fn main() {Grau: steht schon in deiner Datei. Nur zur Orientierung, nicht noch einmal eintippen. let x = 1;Türkiser Balken: das ist neu, das tippst du ein. let y = 2;Roter Balken, durchgestrichen: diese Zeile löschst du (meistens, weil direkt darunter der Ersatz steht). ⋮Drei Punkte: hier steht Code, den wir gerade nicht anfassen.

Nach fast jedem Schritt machen wir einen Probelauf und schauen, ob das Programm tut, was es soll. Kleine Schritte, oft prüfen: So findest du Tippfehler sofort und nicht erst zwanzig Minuten später.

Abgehängt? Kein Problem. Am Ende der Etappe findest du unter Zwischenstand den kompletten Code, so wie er nach Schritt 8 aussehen muss. Damit kannst du jederzeit vergleichen oder neu einsteigen. Und ein Tipp aus Erfahrung: Tipp den Code lieber ab, statt ihn zu kopieren. Deine Finger lernen mit.
Schritt 1

Das Projekt anlegen

Jedes Rust-Programm beginnt mit einem Projektordner. Den legt cargo für uns an. Öffne ein Terminal, geh in den Ordner, in dem du deine Projekte sammelst, und tippe:

cargo new faehrmann
cd faehrmann

cargo new erzeugt einen Ordner faehrmann mit allem, was ein Rust-Projekt braucht. Mit cd gehen wir hinein. Als Nächstes holen wir uns iced dazu, die Bibliothek, mit der wir Fenster, Texte und Knöpfe bauen:

cargo add iced@0.14

Wirf danach einen Blick in die Datei Cargo.toml. Das ist sozusagen der Personalausweis deines Projekts. Ganz unten, unter [dependencies], steht jetzt iced:

[package]
name = "faehrmann"
version = "0.1.0"
edition = "2024"

[dependencies]
iced = "0.14"
Worauf du achten musst Im Projektnamen stehen bewusst ae statt ä: Rust erlaubt in Projektnamen nur einfache Buchstaben, Ziffern, - und _. Im Fenstertitel darf später natürlich „Fährmann“ stehen. Und mit @0.14 legen wir die Version von iced fest. So funktioniert diese Anleitung auch dann noch, wenn irgendwann eine neuere iced-Version erscheint, die manches anders macht.
Schritt 2

Das erste Fenster

Jetzt wird es spannend: Wir bringen ein Fenster auf den Bildschirm. Öffne src/main.rs. Dort steht schon ein kleines „Hello, world!“-Programm, das cargo new für uns angelegt hat. Das brauchen wir nicht. Lösch den ganzen Inhalt der Datei und tippe stattdessen Folgendes ein. Das ist übrigens das einzige Mal, dass wir eine Datei komplett schreiben. Ab Schritt 3 kommen nur noch einzelne Stellen dazu.

src/main.rsdie komplette Datei (alles Alte vorher löschen)
use iced::widget::text;
use iced::Element;

fn main() -> iced::Result {
    iced::application(Faehrmann::default, Faehrmann::update, Faehrmann::view)
        .title("Fährmann")
        .run()
}

#[derive(Default)]
struct Faehrmann {}

#[derive(Debug, Clone)]
enum Message {}

impl Faehrmann {
    fn update(&mut self, message: Message) {
        match message {}
    }

    fn view(&self) -> Element<'_, Message> {
        text("Ahoi! Hier entsteht der Fährmann.").into()
    }
}

Das sind nur gut 20 Zeilen, aber es steckt schon das ganze Prinzip von iced darin. Gehen wir sie gemeinsam durch, von oben nach unten:

  • use … holt Dinge aus iced in unsere Datei, damit wir sie mit ihrem kurzen Namen benutzen können. text ist das Widget, das einen Text anzeigt, Element ist der Oberbegriff für „irgendetwas, das man anzeigen kann“.
  • fn main() ist der Startpunkt jedes Rust-Programms. Hier sagen wir iced: „Starte eine Anwendung.“ Dafür bekommt iced drei Dinge von uns: wie man den Anfangszustand erzeugt (Faehrmann::default), was bei einem Ereignis passieren soll (Faehrmann::update) und wie der Bildschirm aussieht (Faehrmann::view). Mit .title(…) setzen wir den Fenstertitel, und .run() startet das Ganze.
  • struct Faehrmann ist das Gedächtnis unseres Programms. Hier wird später stehen, welcher Ordner gerade offen ist oder welche Datei markiert ist. Im Moment ist es noch leer, und das ist völlig in Ordnung.
  • enum Message ist der Briefkasten. Jedes Ereignis, auf das unser Programm reagieren soll (eine Taste wurde gedrückt, eine Datei ist fertig kopiert), wird ein Brief in diesem Kasten. Noch gibt es keine Ereignisse, also ist auch der Briefkasten leer.
  • update öffnet die Briefe und entscheidet, was passiert. Weil es noch keine Briefe gibt, steht in match message {} nichts drin.
  • view beschreibt, was auf dem Bildschirm zu sehen ist. Im Moment: ein einziger Text. Das .into() am Ende verpackt den Text in ein Element, denn genau das will iced von view zurückbekommen.

Dieses Dreieck aus Zustand, update und view wird uns durch das ganze Projekt begleiten. Im Buch gibt es dazu eine ausführliche Erklärung: Buch, Kapitel 17.

Worauf du achten musst Bei Faehrmann::default, Faehrmann::update und Faehrmann::view stehen keine runden Klammern dahinter. Wir rufen die Funktionen hier nicht selbst auf, sondern geben sie iced in die Hand, und iced ruft sie dann auf, wann immer es sie braucht. Das kleine #[derive(Default)] über dem struct sorgt übrigens dafür, dass es Faehrmann::default überhaupt gibt.
Probelauf Speichern, dann im Terminal:
cargo run
Der erste Start dauert ein paar Minuten, weil cargo iced und alles, was dazugehört, einmal übersetzt. Das passiert nur beim ersten Mal. Danach geht jeder Start nach einer Änderung in wenigen Sekunden. Wenn alles klappt, öffnet sich ein Fenster mit dem Titel „Fährmann“ und dem Text Ahoi! Hier entsteht der Fährmann. oben links. Schließ es einfach wieder, bevor wir weitermachen.
Schritt 3

Schrift und Fenstergröße

Beim Norton Commander ist jeder Buchstabe genau gleich breit, ein „i“ nimmt genauso viel Platz ein wie ein „M“. So eine Schrift heißt Monospace- oder Festbreitenschrift, und sie ist nicht nur Nostalgie: Später, wenn wir Dateinamen und Dateigrößen in Spalten anzeigen, stehen dadurch alle Zahlen sauber untereinander. Also stellen wir die Schrift gleich jetzt für das ganze Programm um. Und wenn wir schon dabei sind, geben wir dem Fenster eine Größe, in der zwei Panels später gut nebeneinander passen.

Zuerst brauchen wir Font aus iced. Ändere dazu die zweite use-Zeile ganz oben in der Datei:

src/main.rsganz oben, zweite Zeile
use iced::widget::text;use iced::Element;use iced::{Element, Font};

Die geschweiften Klammern bedeuten: „Hol mir aus iced gleich mehrere Dinge auf einmal.“ Jetzt ergänzen wir in main zwei Zeilen direkt unter dem Titel:

src/main.rsin fn main(), direkt unter .title(…)
fn main() -> iced::Result {
    iced::application(Faehrmann::default, Faehrmann::update, Faehrmann::view)
        .title("Fährmann")        .default_font(Font::MONOSPACE)
        .window_size((1000.0, 640.0))        .run()
}

default_font legt die Schrift fest, die überall verwendet wird, wenn wir nichts anderes sagen. Font::MONOSPACE ist die Festbreitenschrift deines Betriebssystems. window_size bekommt Breite und Höhe in Pixeln. Die doppelten runden Klammern sind kein Tippfehler: Die inneren Klammern packen die zwei Zahlen zu einem Paar zusammen, die äußeren gehören zum Funktionsaufruf.

Worauf du achten musst Die neuen Zeilen beginnen mit einem Punkt und enden ohne Semikolon. Sie hängen sich an die Zeile davor an wie Waggons an einen Zug: application(…) ist die Lok, .title(…), .default_font(…) und .window_size(…) sind Waggons, und .run() ist der letzte Wagen. Ein Semikolon mittendrin würde den Zug auseinanderreißen, und Rust würde meckern.
Probelauf Speichern, dann im Terminal:
cargo run
Das Fenster ist jetzt deutlich größer, und der Text sieht aus wie von einer alten Schreibmaschine getippt. Genau so soll es sein.
Schritt 4

Die Commander-Farben

Jetzt kommt das Blau. Der Norton Commander stammt aus einer Zeit, in der Grafikkarten nur 16 Farben kannten. Das typische Commander-Blau war genau eine davon, und auch das Türkis der Menüleiste. In Zahlen: Jede Farbe auf dem Bildschirm mischt sich aus Rot, Grün und Blau, jeweils mit einem Wert von 0 bis 255. Das alte Blau war kein Rot, kein Grün, Blau auf 170.

Wir brauchen diese Farben an mehreren Stellen im Programm. Statt die Zahlen jedes Mal neu hinzuschreiben, geben wir ihnen einmal einen Namen. Dafür gibt es in Rust const, also eine Konstante: ein Wert mit Namen, der sich nie ändert. Zuerst holen wir Color dazu:

src/main.rsganz oben, zweite Zeile
use iced::widget::text;use iced::{Element, Font};use iced::{Color, Element, Font};

Dann schreiben wir unsere Farbpalette zwischen die use-Zeilen und fn main():

src/main.rszwischen den use-Zeilen und fn main()
use iced::{Color, Element, Font};// Die Farben des Norton Commander
const BLAU: Color = Color::from_rgb8(0, 0, 170);
const TUERKIS: Color = Color::from_rgb8(0, 170, 170);
const SCHWARZ: Color = Color::BLACK;
const WEISS: Color = Color::WHITE;fn main() -> iced::Result {

Jede Zeile liest sich so: „Es gibt eine Konstante namens BLAU, sie ist vom Typ Color, und ihr Wert ist die Farbe mit Rot 0, Grün 0, Blau 170.“ Für Schwarz und Weiß hat iced schon fertige Farben, die wir einfach übernehmen. Die Großbuchstaben sind in Rust die übliche Schreibweise für Konstanten. So siehst du später an jeder Stelle im Code sofort, dass es sich um einen festen Wert handelt.

Worauf du achten musst In Namen von Konstanten (und Variablen, und Funktionen) verzichten wir auf Umlaute: TUERKIS statt TÜRKIS. Rust würde Umlaute in Namen sogar erlauben, aber es ist unter Rust-Programmierern weltweit üblich, Namen nur aus einfachen Buchstaben zu bilden, und auf englischen Tastaturen lassen sie sich schlecht tippen. Die Faustregel: Umlaute gern in Texten, die der Benutzer sieht, nicht in Namen im Code.
Probelauf Speichern, dann im Terminal:
cargo run
Das Fenster sieht noch genauso aus wie vorher, denn wir haben die Farben ja nur bereitgelegt, aber noch nicht benutzt. Genau darauf weist dich Rust auch hin: Im Terminal erscheinen vier gelbe Warnungen wie constant `BLAU` is never used („die Konstante BLAU wird nie benutzt“). Keine Sorge: Gelb heißt Warnung, nicht Fehler. Das Programm läuft trotzdem. Rust ist hier nur aufmerksam und sagt: „Du hast da etwas vorbereitet, das du noch nicht verwendest.“ Im nächsten Schritt ändern wir das.
Schritt 5

Die blaue Fläche

Ein Text allein hat keinen Hintergrund. Um ihm einen zu geben, packen wir ihn in einen Container. Stell dir einen Container als Kiste vor: Die Kiste kann eine Farbe haben, eine bestimmte Größe, und sie kann ihren Inhalt in die Mitte schieben. Genau das brauchen wir: eine große blaue Kiste, die das ganze Fenster ausfüllt.

Dafür brauchen wir wieder ein paar neue Dinge aus iced. Diesmal ändern sich beide use-Zeilen:

src/main.rsganz oben, die ersten beiden Zeilen
use iced::widget::text;use iced::widget::{container, text};use iced::{Color, Element, Font};use iced::{Background, Color, Element, Fill, Font, Theme};

Jetzt geht es an die Funktion view. Die bisherige Zeile mit dem einzelnen Text ersetzen wir durch unsere Kiste:

src/main.rsin impl Faehrmann, Funktion view
    fn view(&self) -> Element<'_, Message> {        text("Ahoi! Hier entsteht der Fährmann.").into()        let arbeitsflaeche = container(text("Ahoi! Hier entsteht der Fährmann."))
            .center(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(BLAU)),
                text_color: Some(WEISS),
                ..container::Style::default()
            });

        arbeitsflaeche.into()    }
}

Das ist der längste Code-Brocken bisher, also schauen wir ihn uns in Ruhe an:

  • let arbeitsflaeche = container(…) baut die Kiste mit unserem Text darin und gibt ihr den Namen arbeitsflaeche.
  • .center(Fill) sagt: „Mach die Kiste so groß wie möglich (Fill heißt: den ganzen verfügbaren Platz ausfüllen) und stell den Inhalt genau in die Mitte.“
  • .style(…) legt das Aussehen fest. Was in den Klammern steht, ist eine kleine Funktion ohne Namen, eine sogenannte Closure. Zwischen den senkrechten Strichen |…| steht, was sie bekommt: das aktuelle Farbschema (Theme). Das brauchen wir nicht, deshalb der Unterstrich vor dem Namen. Er sagt Rust: „Ich weiß, dass ich das nicht benutze, alles gut.“ Mehr über Closures: Buch, Kapitel 13.
  • Die Closure gibt einen container::Style zurück, also eine Beschreibung des Aussehens: Hintergrund blau, Textfarbe weiß. Das Some(…) heißt: „Ja, hier gibt es einen Wert“ (das Gegenstück wäre None, „nichts festgelegt“).
  • ..container::Style::default() ist ein praktischer Trick: „Alles, was ich nicht ausdrücklich genannt habe (Rahmen, Schatten und so weiter), nimm bitte so wie üblich.“
  • Die letzte Zeile arbeitsflaeche.into() gibt die Kiste an iced zurück, wieder verpackt als Element.
Worauf du achten musst Achte auf das Semikolon nach der schließenden Klammer von .style(…): Dort endet der let-Befehl. In der letzten Zeile, arbeitsflaeche.into(), steht dagegen kein Semikolon. In Rust ist die letzte Zeile ohne Semikolon das, was eine Funktion zurückgibt. Mit Semikolon würde view nichts zurückgeben, und der Compiler würde sich beschweren.
Probelauf Speichern, dann im Terminal:
cargo run
Das ganze Fenster ist jetzt blau, und der Text steht weiß in der Mitte. Das sieht schon ziemlich nach Commander aus! Von den vier gelben Warnungen sind noch zwei übrig: TUERKIS und SCHWARZ warten noch auf ihren Einsatz.
Ein komplett blaues Fenster mit weißem Text in der Mitte
Stand nach Schritt 5
Schritt 6

Die Menüzeile

Ganz oben hatte der Norton Commander eine türkise Zeile mit den Menüs. Die bauen wir jetzt, und zwar nach genau demselben Rezept wie eben: ein Container mit Text, nur diesmal mit türkisem Hintergrund und schwarzer Schrift.

Dabei stoßen wir auf eine neue Frage: Wie bekommen wir zwei Dinge untereinander auf den Bildschirm, die Menüzeile oben und die blaue Fläche darunter? Die Antwort heißt column!, eine Spalte. Alles, was du in eine Spalte packst, stapelt iced von oben nach unten übereinander. Dafür brauchen wir column in der ersten use-Zeile:

src/main.rsganz oben, erste Zeile
use iced::widget::{container, text};use iced::widget::{column, container, text};

Nun bauen wir in view die Menüzeile, und zwar direkt über der Arbeitsfläche. Außerdem ändern wir die letzte Zeile, sodass beide Teile zusammen in einer Spalte landen:

src/main.rsin der Funktion view
    fn view(&self) -> Element<'_, Message> {        let menuzeile = container(text("  Links   Datei   Befehle   Optionen   Rechts"))
            .width(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(TUERKIS)),
                text_color: Some(SCHWARZ),
                ..container::Style::default()
            });        let arbeitsflaeche = container(text("Ahoi! Hier entsteht der Fährmann."))            ⋮            });        arbeitsflaeche.into()        column![menuzeile, arbeitsflaeche].into()    }

Bei der Menüzeile ist .width(Fill) wichtig: Sie soll so breit sein wie das ganze Fenster, aber nur so hoch wie eine Textzeile. Deshalb sagen wir hier nur etwas über die Breite. Die Leerzeichen im Text sorgen für den Abstand zwischen den Menüs. Weil jeder Buchstabe gleich breit ist (danke, Monospace-Schrift!), klappt das zuverlässig.

Das Ausrufezeichen hinter column zeigt dir, dass es ein Makro ist. Ein Makro ist eine Art Abkürzung, die Rust vor dem Übersetzen in längeren Code verwandelt. Für dich heißt das nur: Ausrufezeichen und eckige Klammern nicht vergessen.

Gut beobachtet? Vielleicht ist dir aufgefallen, dass die .style(…)-Teile von Menüzeile und Arbeitsfläche fast gleich aussehen. Das ist tatsächlich doppelter Code. Den räumen wir in einer späteren Etappe auf, wenn wir sehen, welche Farbkombinationen wir insgesamt brauchen. Erst bauen, dann aufräumen, das ist beim Programmieren oft der bessere Weg.
Probelauf Speichern, dann im Terminal:
cargo run
Oben erscheint die türkise Menüzeile mit Links, Datei, Befehle, Optionen, Rechts, darunter die blaue Fläche. Die Menüs lassen sich noch nicht anklicken, sie sind vorerst nur Dekoration. Die Warnung zu TUERKIS ist verschwunden, nur SCHWARZ ist noch übrig.
Schritt 7

Eine einzelne Funktionstaste

Jetzt zur berühmtesten Zeile des Commanders: der Funktionstastenleiste ganz unten. Sie besteht aus zehn Tasten, und jede sieht gleich aus: eine weiße Zahl auf schwarzem Grund, daneben ein türkises Feld mit der Beschriftung.

Wenn etwas zehnmal gleich aussieht, schreiben wir es nicht zehnmal hin. Stattdessen bauen wir eine Funktion, die eine Taste baut. Der geben wir die Nummer und die Beschriftung, und sie liefert uns die fertige Taste. Wie eine Plätzchen-Ausstechform: einmal gebaut, beliebig oft benutzt.

Um die Zahl und das Feld nebeneinander zu stellen, brauchen wir das Gegenstück zu column!: row!, eine Reihe von links nach rechts. Also erst mal wieder die use-Zeile:

src/main.rsganz oben, erste Zeile
use iced::widget::{column, container, text};use iced::widget::{column, container, row, text};

Die neue Funktion kommt ganz ans Ende der Datei, hinter die letzte schließende geschweifte Klammer von impl Faehrmann. Sie gehört also nicht zu unserem Faehrmann, sondern steht für sich allein:

src/main.rsganz am Ende der Datei
        column![menuzeile, arbeitsflaeche].into()
    }
}// Baut eine einzelne Taste der Funktionstastenleiste, z. B. "5 Kopie".
fn f_taste(nummer: u8, beschriftung: &'static str) -> Element<'static, Message> {
    let zahl = text(nummer).color(WEISS);

    let feld = container(text(beschriftung))
        .width(Fill)
        .style(|_theme: &Theme| container::Style {
            background: Some(Background::Color(TUERKIS)),
            text_color: Some(SCHWARZ),
            ..container::Style::default()
        });

    row![zahl, feld].width(Fill).into()
}

Die erste Zeile ist die spannendste, also Stück für Stück:

  • nummer: u8 ist die Tastennummer. u8 ist eine kleine ganze Zahl von 0 bis 255, mehr als genug für zehn Tasten.
  • beschriftung: &'static str ist der Text auf der Taste. Das &str kennst du vielleicht schon: ein geliehener Text. Das 'static bedeutet: „Dieser Text lebt so lange wie das ganze Programm.“ Das stimmt für alles, was direkt in Anführungszeichen im Code steht, also auch für unsere Tastenbeschriftungen.
  • -> Element<'static, Message> heißt: Die Funktion liefert ein Element zurück, also etwas Anzeigbares. Weil es nur Texte enthält, die ewig leben, darf auch das Element ewig leben.

Im Inneren passiert nichts Neues: text(nummer) macht aus der Zahl einen Text, .color(WEISS) färbt ihn weiß, das Feld mit der Beschriftung ist wieder ein türkiser Container. Mit row![zahl, feld] kommen beide nebeneinander, und .width(Fill) sorgt dafür, dass sich die Taste so breit macht, wie Platz da ist. Das wird im nächsten Schritt wichtig, wenn sich zehn Tasten den Platz teilen.

Eine Ausstechform nützt nur etwas, wenn man sie benutzt. Also probieren wir unsere Funktion gleich aus und hängen eine einzelne Taste unten an die Spalte an:

src/main.rsin der Funktion view, letzte Zeile
        column![menuzeile, arbeitsflaeche].into()        column![menuzeile, arbeitsflaeche, f_taste(5, "Kopie")].into()
Worauf du achten musst Die Funktion f_taste muss außerhalb von impl Faehrmann { … } stehen. Wenn du sie aus Versehen vor die letzte schließende Klammer schreibst, wird sie zu einer Methode von Faehrmann, und der Aufruf f_taste(5, "Kopie") findet sie nicht mehr. Die Fehlermeldung lautet dann cannot find function `f_taste`. Zähl im Zweifel die Klammern nach, oder lass deinen Editor den Code mit cargo fmt einrücken, dann sieht man es sofort.
Probelauf Speichern, dann im Terminal:
cargo run
Ganz unten im Fenster erscheint eine einzelne, fensterbreite türkise Taste mit der Beschriftung Kopie. Und die letzte gelbe Warnung ist verschwunden, denn SCHWARZ ist jetzt die Schriftfarbe der Beschriftung.
Wo ist die 5? Wahrscheinlich siehst du links neben „Kopie“ nur ein kleines weißes (oder, bei dunklem Systemdesign, dunkles) Kästchen, aber keine Zahl. Die 5 ist trotzdem da: Sie ist weiß, und der Fensterhintergrund hinter ihr ist auch weiß. Weiß auf Weiß kann man nicht lesen. Den schwarzen Hintergrund, auf dem die Zahlen leuchten sollen, bekommt die ganze Leiste erst im nächsten Schritt. So etwas passiert beim Programmieren ständig: Ein Teil ist fertig, sieht aber erst richtig aus, wenn der nächste Teil dazukommt.
Schritt 8

Die ganze Tastenleiste

Unsere Ausstechform funktioniert. Jetzt stechen wir alle zehn Tasten aus und legen sie in eine Reihe. Die Beschriftungen orientieren sich am Original, das auf Englisch Help, Menu, View, Edit, Copy, RenMov, Mkdir, Delete, PullDn und Quit hieß, nur eben auf Deutsch und kurz genug, dass sie in die Felder passen.

Die neue Leiste schreiben wir in view direkt über die letzte Zeile, und in der letzten Zeile tauschen wir die Probe-Taste gegen die ganze Leiste aus:

src/main.rsin der Funktion view, vor der letzten Zeile
        ⋮                ..container::Style::default()
            });        let tastenleiste = row![
            f_taste(1, "Hilfe"),
            f_taste(2, "Menü"),
            f_taste(3, "Ansicht"),
            f_taste(4, "Bearb."),
            f_taste(5, "Kopie"),
            f_taste(6, "Versch."),
            f_taste(7, "Ordner"),
            f_taste(8, "Lösch."),
            f_taste(9, "Leiste"),
            f_taste(10, "Ende"),
        ]
        .spacing(4);

        let tastenleiste = container(tastenleiste)
            .width(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(SCHWARZ)),
                ..container::Style::default()
            });        column![menuzeile, arbeitsflaeche, f_taste(5, "Kopie")].into()        column![menuzeile, arbeitsflaeche, tastenleiste].into()    }

Zwei Dinge sind hier neu:

  • .spacing(4) lässt zwischen den Tasten jeweils 4 Pixel Abstand. Weil die Leiste einen schwarzen Hintergrund bekommt, sieht man in diesen Lücken Schwarz, genau wie beim Original. Und hinter den weißen Zahlen ist jetzt ebenfalls Schwarz: Die verschwundene 5 aus Schritt 7 taucht wieder auf.
  • Wir schreiben zweimal let tastenleiste. Beim zweiten Mal nehmen wir die fertige Reihe, packen sie in einen schwarzen Container und geben dem Ergebnis denselben Namen. Das nennt man in Rust Shadowing (Überschatten): Der neue Name verdeckt den alten. Das ist praktisch, wenn man etwas Schritt für Schritt verfeinert und sich keine Namen wie tastenleiste2 ausdenken möchte.

Weil jede Taste .width(Fill) hat, teilen sich die zehn Tasten die Breite gerecht auf. Machst du das Fenster breiter oder schmaler, wachsen und schrumpfen alle Tasten gleichmäßig mit. Probier es ruhig aus!

Probelauf Speichern, dann im Terminal:
cargo run
Da ist er: der Fährmann im Commander-Look! Türkise Menüzeile oben, blaue Fläche in der Mitte, und unten die komplette Leiste von 1 Hilfe bis 10 Ende. Im Terminal sollte jetzt keine einzige gelbe Warnung mehr stehen.
Das fertige Fenster aus Etappe 1: Menüzeile, blaue Fläche, Funktionstastenleiste
Stand nach Schritt 8, das Ziel dieser Etappe

Was passiert, wenn du jetzt F10 drückst? Noch gar nichts. Die Tasten sind bisher nur gemalt, das Programm hört noch nicht auf die Tastatur. Das und die ersten echten Dateilisten kommen in den nächsten Etappen.

Zwischenstand

Der komplette Code nach Etappe 1

Hier ist der vollständige Stand, so wie deine Dateien jetzt aussehen sollten. Nimm ihn zum Vergleichen, wenn irgendwo ein Fehler steckt, den du nicht findest, oder als Einstiegspunkt, wenn du erst bei Etappe 2 dazustößt.

Cargo.toml und src/main.rs anzeigen

Cargo.toml

[package]
name = "faehrmann"
version = "0.1.0"
edition = "2024"

[dependencies]
iced = "0.14"

src/main.rs

use iced::widget::{column, container, row, text};
use iced::{Background, Color, Element, Fill, Font, Theme};

// Die Farben des Norton Commander
const BLAU: Color = Color::from_rgb8(0, 0, 170);
const TUERKIS: Color = Color::from_rgb8(0, 170, 170);
const SCHWARZ: Color = Color::BLACK;
const WEISS: Color = Color::WHITE;

fn main() -> iced::Result {
    iced::application(Faehrmann::default, Faehrmann::update, Faehrmann::view)
        .title("Fährmann")
        .default_font(Font::MONOSPACE)
        .window_size((1000.0, 640.0))
        .run()
}

#[derive(Default)]
struct Faehrmann {}

#[derive(Debug, Clone)]
enum Message {}

impl Faehrmann {
    fn update(&mut self, message: Message) {
        match message {}
    }

    fn view(&self) -> Element<'_, Message> {
        let menuzeile = container(text("  Links   Datei   Befehle   Optionen   Rechts"))
            .width(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(TUERKIS)),
                text_color: Some(SCHWARZ),
                ..container::Style::default()
            });

        let arbeitsflaeche = container(text("Ahoi! Hier entsteht der Fährmann."))
            .center(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(BLAU)),
                text_color: Some(WEISS),
                ..container::Style::default()
            });

        let tastenleiste = row![
            f_taste(1, "Hilfe"),
            f_taste(2, "Menü"),
            f_taste(3, "Ansicht"),
            f_taste(4, "Bearb."),
            f_taste(5, "Kopie"),
            f_taste(6, "Versch."),
            f_taste(7, "Ordner"),
            f_taste(8, "Lösch."),
            f_taste(9, "Leiste"),
            f_taste(10, "Ende"),
        ]
        .spacing(4);

        let tastenleiste = container(tastenleiste)
            .width(Fill)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(SCHWARZ)),
                ..container::Style::default()
            });

        column![menuzeile, arbeitsflaeche, tastenleiste].into()
    }
}

// Baut eine einzelne Taste der Funktionstastenleiste, z. B. "5 Kopie".
fn f_taste(nummer: u8, beschriftung: &'static str) -> Element<'static, Message> {
    let zahl = text(nummer).color(WEISS);

    let feld = container(text(beschriftung))
        .width(Fill)
        .style(|_theme: &Theme| container::Style {
            background: Some(Background::Color(TUERKIS)),
            text_color: Some(SCHWARZ),
            ..container::Style::default()
        });

    row![zahl, feld].width(Fill).into()
}
ZusammenfassungEin iced-Programm besteht aus einem Zustand (struct), einer update-Funktion für Ereignisse (Message) und einer view-Funktion für den Bildschirm. Mit container geben wir Dingen Farbe und Größe, mit column! stapeln wir sie übereinander, mit row! legen wir sie nebeneinander. Wiederkehrende Bausteine wie die Funktionstasten bauen wir einmal als Funktion und benutzen sie dann beliebig oft. Und Farben, die wir mehrfach brauchen, stehen als const an einer einzigen Stelle.
Übungen

Zum Ausprobieren

Die Übungen sind freiwillig, aber sie helfen enorm. Mach nach jeder Übung einen Probelauf, und stell danach den alten Zustand wieder her, damit Etappe 2 bei dir genauso weitergeht wie bei allen anderen.

  1. Farbspiel: Ändere den Wert von BLAU auf Color::from_rgb8(40, 40, 40). Was ändert sich im Fenster, was nicht? Und warum musstest du dafür nur eine einzige Zeile anfassen?
  2. Lücken: Nimm in view das .spacing(4) bei der Tastenleiste heraus. Was siehst du? Warum sieht die Leiste danach weniger nach Commander aus?
  3. Leerer Briefkasten: In update steht match message {}, ganz ohne Fälle. Normalerweise verlangt Rust, dass ein match alle Möglichkeiten abdeckt. Warum ist Rust hier trotzdem zufrieden?
Lösungen anzeigen

1. Die große Fläche wird dunkelgrau, die Menüzeile und die Tastenleiste bleiben, wie sie sind. Weil die Farbe als Konstante an genau einer Stelle steht, ändert sich überall dort, wo BLAU verwendet wird, die Farbe automatisch mit. Das ist der große Vorteil von Konstanten: eine Stelle ändern, überall wirksam.

2. Die türkisen Felder stoßen direkt aneinander und verschmelzen zu einem langen Balken. Man sieht nicht mehr, wo eine Taste aufhört und die nächste anfängt, nur die schwarzen Zahlenfelder trennen sie noch. Erst die schmalen schwarzen Lücken machen aus dem Balken wieder zehn einzelne Tasten.

3. Weil Message (noch) gar keine Varianten hat. Ein match muss alle möglichen Fälle abdecken, und bei null möglichen Fällen ist das mit null Zweigen erledigt. Rust weiß sogar, dass es von einem leeren Enum nie einen Wert geben kann, update also im Moment nie aufgerufen wird. Sobald wir in einer späteren Etappe die erste Variante hinzufügen, verlangt Rust von uns auch den passenden Zweig. Und genau das ist gut so: So kann man keinen Fall vergessen.

Etappe 1 geschafft ⛴️

Du hast ein Rust-Projekt angelegt, dein erstes iced-Fenster gebaut und ihm den Look des Norton Commanders gegeben. In der nächsten Etappe kommt Leben in die blaue Fläche: Ein Panel zeigt die Dateien aus deinem eigenen Ordner an.

Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.

☕ Kaffee spendieren