Pfeiltasten, Enter und F10
Unser Panel zeigt einen Ordner, aber bewegen kann man sich darin noch nicht. Heute lernt der Fährmann zuhören: Mit den Pfeiltasten wandert ein türkiser Balken durch die Liste, Enter öffnet einen Ordner, und F10 beendet das Programm, genau wie im Original. Dabei lernst du, wie ein iced-Programm auf die Tastatur hört, auch wenn gar kein Knopf angeklickt wird.
Bevor wir loslegen
Wir machen da weiter, wo Etappe 2 aufgehört hat. Den kompletten Stand findest du dort unter Zwischenstand.
Bisher kamen Nachrichten in iced immer von einem Widget: Ein Klick auf eine Fläche verschickt einen Brief. Die Tastatur gehört aber keinem bestimmten Widget, sie gehört dem ganzen Programm. Für so etwas gibt es in iced Abonnements (Subscriptions): Das Programm abonniert eine Quelle von Ereignissen, hier die Tastatur, und bekommt jedes Ereignis als Nachricht zugestellt, wie eine Zeitung, die jeden Morgen im Briefkasten liegt.
Der Auswahlbalken
Im Commander zeigt ein türkiser Balken, welcher Eintrag gerade ausgewählt ist. Dafür muss sich der Fährmann zwei neue Dinge merken: welche Nummer der markierte Eintrag hat, und ab welcher Nummer die Liste angezeigt wird. Das Zweite brauchen wir für Ordner mit mehr als 24 Einträgen, dazu in Schritt 4 mehr.
ordner: PathBuf, eintraege: Vec<Eintrag>,cursor: usize, // Nummer des markierten Eintrags erste_zeile: usize, // Nummer des obersten sichtbaren Eintrags}
Beim Start steht beides auf 0, also ganz oben:
let eintraege = ordner_lesen(&ordner);Faehrmann { ordner, eintraege }Faehrmann { ordner, eintraege, cursor: 0, erste_zeile: 0, }}
Jetzt muss panel_ansicht jeder Zeile mitteilen, ob sie markiert ist. Dafür
muss die Schleife wissen, welche Nummer der jeweilige Eintrag hat:
let mut liste = column![];for eintrag in self.eintraege.iter().take(ZEILEN) { liste = liste.push(zeile(eintrag)); }for (nr, eintrag) in self .eintraege .iter() .enumerate() .skip(self.erste_zeile) .take(ZEILEN) { liste = liste.push(zeile(eintrag, nr == self.cursor)); }
Diese Kette liest sich von oben nach unten wie ein Rezept:
.iter(): Geh die Einträge durch..enumerate(): Nummeriere sie dabei. Statt eines Eintrags bekommen wir jetzt ein Paar aus Nummer und Eintrag, und(nr, eintrag)packt das Paar gleich wieder aus..skip(self.erste_zeile): Überspring die ersten paar. Im Moment sind das null..take(ZEILEN): Nimm höchstens 24.
Wichtig ist die Reihenfolge: Wir nummerieren vor dem Überspringen. So behält jeder
Eintrag seine echte Nummer, und nr == self.cursor stimmt auch dann noch, wenn
wir später ein Stück nach unten geblättert haben.
Bleibt die Funktion zeile. Sie bekommt einen zweiten Wert, und eine markierte
Zeile wird schwarz auf türkis gezeichnet. Drei Stellen ändern sich:
// Baut eine Zeile im Panel: links der Name, rechts die Größe.fn zeile(eintrag: &Eintrag) -> Element<'_, Message> { let farbe = if eintrag.ist_ordner { WEISS } else { HELLTUERKIS };fn zeile(eintrag: &Eintrag, markiert: bool) -> Element<'_, Message> { let farbe = if markiert { SCHWARZ } else if eintrag.ist_ordner { WEISS } else { HELLTUERKIS };let groesse = if eintrag.ist_ordner {⋮row![let inhalt = row![text(eintrag.name.as_str())⋮text(groesse).color(farbe), ].spacing(12) .into().spacing(12); // Der markierte Eintrag bekommt den türkisen Balken container(inhalt) .width(Fill) .style(move |_theme: &Theme| container::Style { background: if markiert { Some(Background::Color(TUERKIS)) } else { None }, ..container::Style::default() }) .into()}
Die Zeile steckt jetzt in einem Container, der die ganze Breite einnimmt. Ist sie
markiert, bekommt der Container einen türkisen Hintergrund. Sonst bekommt er
None, also gar keinen, und das Blau dahinter scheint durch. Neu ist das
move vor der Closure: iced ruft die Closure erst später auf, wenn es die Zeile
zeichnet, und dann gibt es die Funktion zeile mit ihrem Wert
markiert längst nicht mehr. Mit move packt die Closure sich eine
eigene Kopie von markiert ein. Ohne move meldet Rust
closure may outlive the current function und schlägt
move gleich selbst vor.
cargo run
Der Eintrag .. ganz oben hat jetzt einen türkisen Balken mit schwarzer
Schrift. Bewegen lässt er sich noch nicht. Das bauen wir als Nächstes. Rust warnt
übrigens noch nicht, denn cursor und erste_zeile werden ja schon
gelesen.Drei neue Nachrichten
Was soll passieren, wenn eine Taste gedrückt wird? Dreierlei: Der Balken geht hoch, der Balken geht runter, oder das Programm wird beendet. Das sind unsere ersten drei Briefsorten:
#[derive(Debug, Clone)]enum Message {}enum Message { Hoch, Runter, Beenden, }
Und update muss wissen, was bei welchem Brief zu tun ist. Dabei begegnet uns
etwas Neues. Hoch und Runter verändern nur unser Gedächtnis. Aber das Programm zu
beenden, das kann update nicht selbst, das muss iced erledigen. Für
solche Aufträge an iced gibt es den Task, eine Aufgabe, die
update an iced zurückgibt. Den holen wir uns:
use iced::{Background, Border, Color, Element, Fill, Font, Theme};use iced::{Background, Border, Color, Element, Fill, Font, Task, Theme};
Und so sieht das neue update aus. Ersetze die alte Funktion komplett:
fn update(&mut self, message: Message) { match message {} }fn update(&mut self, message: Message) -> Task<Message> { match message { Message::Hoch => { if self.cursor > 0 { self.cursor -= 1; } } Message::Runter => { if self.cursor + 1 < self.eintraege.len() { self.cursor += 1; } } Message::Beenden => return iced::exit(), } Task::none() }
-> Task<Message>in der ersten Zeile heißt:updateliefert ab jetzt immer eine Aufgabe zurück.- Bei
Hochprüfen wir zuerst, ob wir nicht schon ganz oben sind. Das ist beiusizewichtig: 0 minus 1 gibt es nicht, und Rust würde das Programm im Testmodus mit einer Fehlermeldung anhalten. - Bei
Runterprüfen wir umgekehrt, ob es überhaupt einen nächsten Eintrag gibt.self.eintraege.len()ist die Anzahl der Einträge. Beendengibt sofort die Aufgabeiced::exit()zurück: „iced, bitte beende das Programm.“returnverlässt die Funktion an dieser Stelle.- Für alle anderen Fälle liefern wir am Ende
Task::none()zurück, die leere Aufgabe: „Danke iced, es gibt nichts weiter zu tun.“
Task::none() steht am Ende ohne Semikolon, denn es ist der
Rückgabewert. Und nach return iced::exit() steht im match-Zweig ein
Komma, kein Semikolon, weil es ein kurzer Zweig ohne geschweifte Klammern ist.cargo run
Noch passiert nichts, denn keiner verschickt diese Briefe. Rust merkt das auch und
warnt, dass Hoch, Runter und Beenden nie erzeugt
werden.Die Tastatur abonnieren
Jetzt verbinden wir die Tastatur mit unseren Briefen. Dafür holen wir zuerst das Tastatur-Werkzeug von iced, die Namen der Sondertasten und den Typ für Abonnements:
use iced::widget::{column, container, row, text};use iced::{Background, Border, Color, Element, Fill, Font, Task, Theme};use iced::keyboard::{self, Key, key::Named}; use iced::{Background, Border, Color, Element, Fill, Font, Subscription, Task, Theme};
keyboard::{self, …} holt das Modul keyboard selbst (das ist das
self) und daraus Key, den Typ für eine Taste.
key::Named sind die Tasten mit Namen, also Pfeile, Enter, F1 bis F12 und so
weiter. Das Abonnement selbst ist eine kurze Funktion in impl Faehrmann:
Message::Beenden => return iced::exit(), } Task::none() }// Abonniert die Tastatur: Jeder Tastendruck geht durch die Funktion taste. fn subscription(&self) -> Subscription<Message> { keyboard::listen().filter_map(taste) }
keyboard::listen() liefert jedes Tastatur-Ereignis.
.filter_map(taste) schickt jedes davon durch unsere Funktion
taste, die entscheidet, ob daraus ein Brief wird. Die schreiben wir ans Ende
der Datei:
text.replace('.', ",") }// Übersetzt einen Tastendruck in eine Nachricht, oder in gar nichts. fn taste(ereignis: keyboard::Event) -> Option<Message> { let keyboard::Event::KeyPressed { key, .. } = ereignis else { return None; // Taste losgelassen oder Ähnliches: interessiert uns nicht }; match key { Key::Named(Named::ArrowUp) => Some(Message::Hoch), Key::Named(Named::ArrowDown) => Some(Message::Runter), Key::Named(Named::F10) => Some(Message::Beenden), _ => None, } }
Wieder ein let … else, diesmal mit einem enum: Ein
Tastatur-Ereignis kann ein Drücken sein (KeyPressed) oder ein Loslassen und
anderes. Wir wollen nur das Drücken. Von den vielen Angaben darin interessiert uns nur
key, die Taste. Die zwei Punkte .. heißen: „Den Rest ignorieren.“
Danach übersetzt ein match die Taste in eine Nachricht. Alle anderen Tasten
landen bei _ und werden zu None, also zu gar nichts.
Zum Schluss melden wir das Abonnement bei iced an, mit einem weiteren Waggon am Zug in
main:
iced::application(Faehrmann::neu, Faehrmann::update, Faehrmann::view).subscription(Faehrmann::subscription).title("Fährmann")
.filter_map(taste) stehen hinter taste keine
Klammern. Wir geben die Funktion weiter, wir rufen sie nicht auf. Das kennst du schon von
Faehrmann::update und Faehrmann::view.cargo run
Drück die Pfeiltasten: Der türkise Balken wandert durch die Liste! Drück
F10, und das Fenster schließt sich. Fällt dir noch etwas auf? Wenn dein Ordner
mehr als 24 Einträge hat und du weiter nach unten gehst, verschwindet der Balken unten aus
dem Panel. Er ist noch da, nur sieht man ihn nicht. Das reparieren wir jetzt.Ein Fenster, das mitläuft
Stell dir das Panel als Fenster in einem Zug vor, der an einer langen Liste vorbeifährt.
Durch das Fenster siehst du immer 24 Einträge. erste_zeile ist der Eintrag, den
du gerade ganz oben im Fenster siehst. Wenn der Balken unten aus dem Fenster laufen will,
muss der Zug ein Stück weiterfahren. Und wenn er oben herauswill, ein Stück zurück. Genau
das macht eine neue Funktion hinter subscription:
keyboard::listen().filter_map(taste) }// Schiebt den sichtbaren Ausschnitt so, dass der Balken immer zu sehen ist. fn sichtbar_halten(&mut self) { if self.cursor < self.erste_zeile { self.erste_zeile = self.cursor; } if self.cursor >= self.erste_zeile + ZEILEN { self.erste_zeile = self.cursor + 1 - ZEILEN; } }
- Steht der Balken über dem Fenster, also bei einer kleineren Nummer als
erste_zeile, rückt das Fenster nach oben, bis der Balken ganz oben steht. - Steht der Balken unter dem Fenster, also bei
erste_zeile + ZEILENoder später, rückt das Fenster so weit nach unten, dass der Balken in der untersten Zeile steht. Ein kleines Rechenbeispiel: Der Balken steht auf Eintrag 24, das ist der 25. Eintrag (wir zählen ja ab 0). Dann muss das Fenster bei 24 + 1 − 24 = 1 anfangen, also bei Eintrag 1, damit 1 bis 24 zu sehen sind.
Aufrufen müssen wir das nach jeder Bewegung, also zweimal in update:
Message::Hoch => { if self.cursor > 0 { self.cursor -= 1; }self.sichtbar_halten();}⋮if self.cursor + 1 < self.eintraege.len() { self.cursor += 1; }self.sichtbar_halten();}
scrollable, das Inhalte mit einer Scrollleiste versieht. Für den Commander
passt unser eigener Ausschnitt aber besser: Der Balken soll sich mit der Tastatur bewegen,
und das Fenster soll genau so weit mitlaufen wie nötig, nicht weiter. So war es auch im
Original, das ja aus einer Zeit ganz ohne Maus stammt.cargo run
Halt die Pfeiltaste nach unten gedrückt: Wenn der Balken den unteren Rand erreicht,
rollt die Liste mit. Und auf dem Rückweg rollt sie oben wieder zurück.Mit Enter in den Ordner
Jetzt wird der Fährmann ein echter Dateimanager: Steht der Balken auf einem Ordner und du
drückst Enter, geht es hinein. Steht er auf .., geht es eine Ebene
hoch. Zuerst die Briefsorte:
Runter,Oeffnen,Beenden,
Rust schreibt man hier Oeffnen ohne Umlaut. Im update leiten wir den
Brief an eine eigene Funktion weiter, damit update übersichtlich bleibt:
self.cursor += 1; } self.sichtbar_halten(); }Message::Oeffnen => self.oeffnen(),Message::Beenden => return iced::exit(),
Und das ist die Funktion selbst, hinter sichtbar_halten:
self.erste_zeile = self.cursor + 1 - ZEILEN; } }// Betritt den markierten Ordner, oder geht bei ".." eine Ebene hoch. fn oeffnen(&mut self) { let Some(eintrag) = self.eintraege.get(self.cursor) else { return; }; if !eintrag.ist_ordner { return; // Dateien öffnen wir (noch) nicht } let neuer_ordner = if eintrag.name == ".." { match self.ordner.parent() { Some(oben) => oben.to_path_buf(), None => return, } } else { self.ordner.join(&eintrag.name) }; self.eintraege = ordner_lesen(&neuer_ordner); self.ordner = neuer_ordner; self.cursor = 0; self.erste_zeile = 0; }
self.eintraege.get(self.cursor)holt den markierten Eintrag. Anders alsself.eintraege[self.cursor]stürztgetnicht ab, wenn es den Eintrag nicht gibt, zum Beispiel in einem leeren Ordner. Es liefert dannNone, undlet Some(…) … elsebeendet die Funktion.- Bei
..fragen wir mitparent()nach dem übergeordneten Ordner. Das ist eineOption, denn ganz oben gibt es keinen. Dasmatchnimmt im guten Fall den Ordner, macht mitto_path_buf()einen eigenenPathBufdaraus, und im anderen Fall bricht es ab. - Sonst hängen wir mit
joinden Namen an den aktuellen Pfad an. Aus/home/volkerundDokumentewird/home/volker/Dokumente, unter Windows natürlich mit\, darum kümmert sich Rust. - Zum Schluss lesen wir den neuen Ordner, merken ihn uns und setzen Balken und Fenster wieder nach oben.
Bleibt noch, die Enter-Taste zu übersetzen:
Key::Named(Named::ArrowDown) => Some(Message::Runter),Key::Named(Named::Enter) => Some(Message::Oeffnen),Key::Named(Named::F10) => Some(Message::Beenden),
&neuer_ordner, dann geben wir neuer_ordner an
self.ordner weiter. Andersherum ginge es nicht, denn nach dem Weitergeben
gehört der Pfad nicht mehr neuer_ordner, und Rust würde
borrow of moved value melden. Mehr dazu, wem was gehört:
Buch, Kapitel 11.cargo run
Geh mit dem Balken auf einen Ordner und drück Enter: Der Pfad oben
ändert sich, und du siehst den Inhalt des Ordners. Mit .. und
Enter geht es zurück. Du kannst dich jetzt durch deine ganze Festplatte
bewegen!
Der komplette Code nach Etappe 3
Hier ist der vollständige Stand nach Etappe 3. Nimm ihn zum Vergleichen, wenn irgendwo ein Fehler steckt, den du nicht findest, oder als Einstiegspunkt, wenn du erst bei der nächsten Etappe 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 std::fs;
use std::path::{Path, PathBuf};
use iced::widget::{column, container, row, text};
use iced::keyboard::{self, Key, key::Named};
use iced::{Background, Border, Color, Element, Fill, Font, Subscription, Task, 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;
const HELLTUERKIS: Color = Color::from_rgb8(85, 255, 255);
const GELB: Color = Color::from_rgb8(255, 255, 85);
// So viele Einträge passen untereinander in ein Panel
const ZEILEN: usize = 24;
// Ein Eintrag in einem Ordner: eine Datei oder ein Unterordner.
struct Eintrag {
name: String,
ist_ordner: bool,
groesse: u64,
}
fn main() -> iced::Result {
iced::application(Faehrmann::neu, Faehrmann::update, Faehrmann::view)
.subscription(Faehrmann::subscription)
.title("Fährmann")
.default_font(Font::MONOSPACE)
.window_size((1000.0, 640.0))
.run()
}
// Das Gedächtnis des Fährmanns: welcher Ordner offen ist und was darin liegt.
struct Faehrmann {
ordner: PathBuf,
eintraege: Vec<Eintrag>,
cursor: usize, // Nummer des markierten Eintrags
erste_zeile: usize, // Nummer des obersten sichtbaren Eintrags
}
#[derive(Debug, Clone)]
enum Message {
Hoch,
Runter,
Oeffnen,
Beenden,
}
impl Faehrmann {
// Startet im Ordner, aus dem das Programm aufgerufen wurde.
fn neu() -> Self {
let ordner = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
let eintraege = ordner_lesen(&ordner);
Faehrmann {
ordner,
eintraege,
cursor: 0,
erste_zeile: 0,
}
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Hoch => {
if self.cursor > 0 {
self.cursor -= 1;
}
self.sichtbar_halten();
}
Message::Runter => {
if self.cursor + 1 < self.eintraege.len() {
self.cursor += 1;
}
self.sichtbar_halten();
}
Message::Oeffnen => self.oeffnen(),
Message::Beenden => return iced::exit(),
}
Task::none()
}
// Abonniert die Tastatur: Jeder Tastendruck geht durch die Funktion taste.
fn subscription(&self) -> Subscription<Message> {
keyboard::listen().filter_map(taste)
}
// Schiebt den sichtbaren Ausschnitt so, dass der Balken immer zu sehen ist.
fn sichtbar_halten(&mut self) {
if self.cursor < self.erste_zeile {
self.erste_zeile = self.cursor;
}
if self.cursor >= self.erste_zeile + ZEILEN {
self.erste_zeile = self.cursor + 1 - ZEILEN;
}
}
// Betritt den markierten Ordner, oder geht bei ".." eine Ebene hoch.
fn oeffnen(&mut self) {
let Some(eintrag) = self.eintraege.get(self.cursor) else {
return;
};
if !eintrag.ist_ordner {
return; // Dateien öffnen wir (noch) nicht
}
let neuer_ordner = if eintrag.name == ".." {
match self.ordner.parent() {
Some(oben) => oben.to_path_buf(),
None => return,
}
} else {
self.ordner.join(&eintrag.name)
};
self.eintraege = ordner_lesen(&neuer_ordner);
self.ordner = neuer_ordner;
self.cursor = 0;
self.erste_zeile = 0;
}
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(self.panel_ansicht())
.width(Fill)
.height(Fill)
.padding(6)
.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()
}
// Zeichnet das Panel: oben der Pfad, darunter die Einträge, rundherum ein Rahmen.
fn panel_ansicht(&self) -> Element<'_, Message> {
let kopf = text(self.ordner.display().to_string()).color(GELB);
let mut liste = column![];
for (nr, eintrag) in self
.eintraege
.iter()
.enumerate()
.skip(self.erste_zeile)
.take(ZEILEN)
{
liste = liste.push(zeile(eintrag, nr == self.cursor));
}
container(column![kopf, liste].spacing(6))
.width(Fill)
.height(Fill)
.padding(8)
.clip(true)
.style(|_theme: &Theme| container::Style {
border: Border {
color: TUERKIS,
width: 2.0,
radius: 0.0.into(),
},
..container::Style::default()
})
.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()
}
// Liest einen Ordner und liefert seine Einträge: oben "..", dann Ordner, dann Dateien.
fn ordner_lesen(pfad: &Path) -> Vec<Eintrag> {
let mut eintraege = Vec::new();
let Ok(inhalt) = fs::read_dir(pfad) else {
return eintraege; // Ordner nicht lesbar: leere Liste
};
for eintrag in inhalt.flatten() {
let name = eintrag.file_name().to_string_lossy().to_string();
if name.starts_with('.') {
continue; // versteckte Dateien überspringen
}
let Ok(info) = eintrag.metadata() else {
continue; // keine Infos lesbar: Eintrag überspringen
};
eintraege.push(Eintrag {
name,
ist_ordner: info.is_dir(),
groesse: info.len(),
});
}
// Ordner vor Dateien, innerhalb davon alphabetisch
eintraege.sort_by(|a, b| {
b.ist_ordner
.cmp(&a.ist_ordner)
.then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
// Der Weg nach oben steht immer ganz oben
if pfad.parent().is_some() {
let hoch = Eintrag {
name: String::from(".."),
ist_ordner: true,
groesse: 0,
};
eintraege.insert(0, hoch);
}
eintraege
}
// Baut eine Zeile im Panel: links der Name, rechts die Größe.
fn zeile(eintrag: &Eintrag, markiert: bool) -> Element<'_, Message> {
let farbe = if markiert {
SCHWARZ
} else if eintrag.ist_ordner {
WEISS
} else {
HELLTUERKIS
};
let groesse = if eintrag.ist_ordner {
String::from("<ORDNER>")
} else {
groesse_text(eintrag.groesse)
};
let inhalt = row![
text(eintrag.name.as_str())
.color(farbe)
.width(Fill)
.wrapping(text::Wrapping::None),
text(groesse).color(farbe),
]
.spacing(12);
// Der markierte Eintrag bekommt den türkisen Balken
container(inhalt)
.width(Fill)
.style(move |_theme: &Theme| container::Style {
background: if markiert {
Some(Background::Color(TUERKIS))
} else {
None
},
..container::Style::default()
})
.into()
}
// Macht aus einer Zahl von Bytes etwas Lesbares, zum Beispiel "12,3 KB".
fn groesse_text(bytes: u64) -> String {
let b = bytes as f64;
let text = if bytes < 1024 {
format!("{} B", bytes)
} else if b < 1024.0 * 1024.0 {
format!("{:.1} KB", b / 1024.0)
} else if b < 1024.0 * 1024.0 * 1024.0 {
format!("{:.1} MB", b / 1024.0 / 1024.0)
} else {
format!("{:.1} GB", b / 1024.0 / 1024.0 / 1024.0)
};
text.replace('.', ",")
}
// Übersetzt einen Tastendruck in eine Nachricht, oder in gar nichts.
fn taste(ereignis: keyboard::Event) -> Option<Message> {
let keyboard::Event::KeyPressed { key, .. } = ereignis else {
return None; // Taste losgelassen oder Ähnliches: interessiert uns nicht
};
match key {
Key::Named(Named::ArrowUp) => Some(Message::Hoch),
Key::Named(Named::ArrowDown) => Some(Message::Runter),
Key::Named(Named::Enter) => Some(Message::Oeffnen),
Key::Named(Named::F10) => Some(Message::Beenden),
_ => None,
}
}
Subscription).
keyboard::listen().filter_map(…) übersetzt Tastendrücke in Nachrichten.
Aufgaben, die iced erledigen soll, gibt update als Task zurück,
zum Beispiel iced::exit(). Mit enumerate, skip und
take zeigen wir einen Ausschnitt einer langen Liste, und get
greift sicher auf einen Eintrag zu, der vielleicht gar nicht da ist.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 die nächste Etappe bei dir genauso weitergeht wie bei allen anderen.
- Pos1 und Ende: Die Tasten Pos1 und Ende (auf
englischen Tastaturen Home und End) sollen den Balken an den Anfang
und ans Ende der Liste springen lassen. Du brauchst zwei neue Nachrichten und
Named::HomeundNamed::End. - Seitenweise: Mit Bild ↓ (
Named::PageDown) soll der Balken 24 Einträge auf einmal nach unten springen, aber nie über das Ende hinaus. Tipp:.min(…)liefert die kleinere von zwei Zahlen. - Wo komme ich her? Wenn du mit
..nach oben gehst, steht der Balken danach ganz oben. Im Original stand er auf dem Ordner, aus dem man gerade kam. Kannst du das nachbauen?
Lösungen anzeigen
1. Zwei Varianten Anfang und Ende in Message, zwei
Zeilen in taste, und in update:
Message::Anfang => { self.cursor = 0; self.sichtbar_halten(); } und
Message::Ende => { self.cursor = self.eintraege.len().saturating_sub(1);
self.sichtbar_halten(); }. saturating_sub(1) zieht eins ab, bleibt
aber bei 0 stehen, falls die Liste leer ist.
2. self.cursor = (self.cursor + ZEILEN).min(self.eintraege.len().saturating_sub(1));
und danach self.sichtbar_halten();. min sorgt dafür, dass der
Balken nie hinter dem letzten Eintrag landet.
3. Vor dem Wechsel den Namen des aktuellen Ordners merken:
let herkunft = self.ordner.file_name().map(|n| n.to_string_lossy().to_string());.
Nach dem Einlesen suchen: if let Some(name) = herkunft { if let Some(pos) =
self.eintraege.iter().position(|e| e.name == name) { self.cursor = pos; } } und
zum Schluss self.sichtbar_halten();. Beim Hineingehen in einen Ordner findet
die Suche nichts Passendes, und der Balken bleibt oben, genau wie gewünscht.
Etappe 3 geschafft ⌨️
Der Fährmann hört auf die Tastatur: Der Balken wandert, die Liste rollt mit, Enter öffnet Ordner und F10 beendet das Programm. In der nächsten Etappe bekommt der Fährmann sein zweites Panel, und mit der Tab-Taste springst du zwischen beiden hin und her.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren