App-Zustand des offenen Workspace und die Router-Struktur #5

Closed
opened 2026-08-27 12:32:54 +00:00 by RDietermann · 2 comments
Owner

Part of #1

Question

Wie wird der offene Workspace im App-Zustand geführt — und was ersetzt workspace_selected?

Der heutige Stand in src/main.rs: ein Signal<bool> schaltet zwischen Router::<WorkspaceRoute> und Router::<EditorRoute>. Das Signal wird nirgends gesetzt; der Übergang existiert also gar nicht. Ein bool kann außerdem nicht sagen, welcher Workspace offen ist.

Zu entscheiden:

  • Welcher Typ trägt den Zustand? Signal<Option<Workspace>>, ein eigener Enum über die App-Phase, etwas anderes?
  • Wie kommen tief liegende Komponenten (der Selector, später die files-bar) daran — Context-Provider, Props-Durchreichung?
  • Zwei getrennte Router oder ein gemeinsamer? Zwei Router bedeuten zwei Adressräume, die beide / kennen. Ist das tragfähig, oder braucht es eine Route-Ebene darüber?
  • Wo lebt der Rückweg „Workspace wechseln", wenn er später dazukommt — braucht die Struktur ihn schon als Platz, oder ist er dann eine kleine Ergänzung?

Zielgröße: die Typen und ihr Ort im Baum, so konkret, dass man sie hinschreiben kann.

Part of #1 ## Question Wie wird der offene Workspace im App-Zustand geführt — und was ersetzt `workspace_selected`? Der heutige Stand in `src/main.rs`: ein `Signal<bool>` schaltet zwischen `Router::<WorkspaceRoute>` und `Router::<EditorRoute>`. Das Signal wird nirgends gesetzt; der Übergang existiert also gar nicht. Ein `bool` kann außerdem nicht sagen, *welcher* Workspace offen ist. Zu entscheiden: - Welcher Typ trägt den Zustand? `Signal<Option<Workspace>>`, ein eigener Enum über die App-Phase, etwas anderes? - Wie kommen tief liegende Komponenten (der Selector, später die `files-bar`) daran — Context-Provider, Props-Durchreichung? - Zwei getrennte Router oder ein gemeinsamer? Zwei Router bedeuten zwei Adressräume, die beide `/` kennen. Ist das tragfähig, oder braucht es eine Route-Ebene darüber? - Wo lebt der Rückweg „Workspace wechseln", wenn er später dazukommt — braucht die Struktur ihn schon als Platz, oder ist er dann eine kleine Ergänzung? Zielgröße: die Typen und ihr Ort im Baum, so konkret, dass man sie hinschreiben kann.
RDietermann added the wayfinder:grilling label 2026-08-27 12:32:54 +00:00
RDietermann self-assigned this 2026-08-27 14:09:12 +00:00
Author
Owner

Antwort

Ein Router statt zwei. WorkspaceRoute und EditorRoute verschmelzen zu einem Routable-Enum, in dem der Editor-Teil unter #[nest("/workspace")] liegt. Der offene Workspace lebt als Signal<Option<Workspace>> in einem Context-Provider.

Warum nicht bei zwei Routern bleiben

Der heutige Aufbau ist nie gelaufen — workspace_selected wird nirgends auf true gesetzt. Beim ersten Umlegen des Schalters wäre er aufgefallen: beide Enums definieren #[route("/")] und teilen sich dieselbe History. Wer im Selector auf „Open Workspace" klickt, steht auf /ws_open; schaltet die App in diesem Moment auf den EditorRoute-Router um, bekommt der eine URL zu sehen, die er nicht kennt — er hat nur / und /editor. Das ist der wahrscheinlichste Weg durch die App, nicht ein Randfall.

Reparieren ließe sich das mit einer erzwungenen Navigation bei jedem Wechsel. Das behandelt aber das Symptom: zwei Adressräume, die sich eine History teilen, bleiben zwei Adressräume.

Die Struktur

#[derive(Debug, Clone, Routable, PartialEq, EnumMessage)]
#[rustfmt::skip]
pub enum Route {
    #[layout(Selector)]
        #[route("/")]
        #[strum(message = "Create new Workspace")]
        NewWorkspace {},
        #[route("/open")]
        #[strum(message = "Open Workspace")]
        OpenWorkspace {},
    #[end_layout]

    #[nest("/workspace")]
        #[layout(EditorShell)]
            #[route("/")]
            #[strum(message = "Home")]
            Home {},
            #[route("/editor")]
            #[strum(message = "Editor")]
            Editor {},
        #[end_layout]
    #[end_nest]
}

App rendert nur noch Router::<Route> {} — das if workspace_selected() entfällt ersatzlos, ebenso das Signal<bool>.

Der Zustand

pub struct Workspace {
    pub root: PathBuf,
}

// in App, vor dem Router:
use_context_provider(|| Signal::new(None::<Workspace>));

Ein benannter Typ statt Option<PathBuf>: CONTEXT.md führt „Workspace" als Domänenbegriff, der Marker bringt absehbar Einstellungen mit, die hier andocken, und ein nacktes PathBuf im Context zwingt jeden Leser zu der Frage, welcher Pfad gemeint ist — Workspace-Ordner, Notiz oder Konfigurationsverzeichnis.

Context, nicht Props: Der Workspace wird im Selector gesetzt und in der files-bar gelesen. Beide hängen unter einem Outlet, und durch ein Outlet lassen sich keine Props durchreichen. Das ist keine Wahl, sondern eine Randbedingung.

Der Workspace-Pfad gehört nicht in die Route. /workspace/C%3A%5CUsers%5C... wäre die Alternative; sie macht aus einem Dateipfad einen URL-Bestandteil samt Escaping-Fragen und gewinnt in einer Desktop-App ohne Adressleiste nichts.

Der Guard

Das EditorShell-Layout prüft den Context und schickt bei None zurück zum Selector — eine Prüfung an genau der Stelle, an der der Outlet steht. Der Fall ist real: Hot-Reload während der Entwicklung setzt den Context zurück, die Route bleibt bestehen; später kommt „Workspace schließen" dazu.

Ein statisches #[redirect] scheidet aus — es sieht nur die URL, nicht den Zustand.

Beim Bauen zu prüfen: ob die Navigation direkt im Render-Pfad erfolgen darf (navigator().replace(...) plus leeres rsx!) oder in einen use_effect gehört. Navigation ist ein Seiteneffekt, und Dioxus 0.7 ist an dieser Stelle nicht eindeutig dokumentiert. Die Entscheidung „Guard im Layout" ist davon unberührt, nur die Zeile darin.

„Workspace wechseln"

Braucht keinen strukturellen Platzhalter. Mit dem Selector unter / und dem Editor unter /workspace/... ist Wechseln eine Navigation nach / plus Leeren des Context — zwei Zeilen an dem Tag, an dem der Knopf dafür entsteht.


Folge, die beim Ausformulieren aufgefallen ist

Die Tab-Leisten werden heute aus Route::iter() erzeugt (Workspace() im Selector, Navbar() im Editor). Mit einem Enum liefert iter() alle vier Varianten — beide Leisten würden die Routen des jeweils anderen Bereichs mitlisten.

Vorschlag, minimal und ohne Filterlogik:

impl Route {
    pub const SELECTOR_TABS: [Self; 2] = [Self::NewWorkspace {}, Self::OpenWorkspace {}];
    pub const EDITOR_TABS: [Self; 2] = [Self::Home {}, Self::Editor {}];
}

Die Labels bleiben über #[strum(message = ...)] an den Varianten, EnumIter wird nicht mehr gebraucht. Nachteil, offen ausgesprochen: eine neue Route zwingt einen nicht, das passende Array zu ergänzen — der Compiler prüft das nicht. Die Alternative wäre Route::iter().filter(|r| matches!(...)), was denselben Pflegepunkt an eine unauffälligere Stelle schiebt.

Klein genug, um es beim Bauen zu entscheiden — hier festgehalten, damit es nicht überrascht.

## Antwort **Ein Router statt zwei.** `WorkspaceRoute` und `EditorRoute` verschmelzen zu einem `Routable`-Enum, in dem der Editor-Teil unter `#[nest("/workspace")]` liegt. Der offene Workspace lebt als `Signal<Option<Workspace>>` in einem Context-Provider. ### Warum nicht bei zwei Routern bleiben Der heutige Aufbau ist nie gelaufen — `workspace_selected` wird nirgends auf `true` gesetzt. Beim ersten Umlegen des Schalters wäre er aufgefallen: beide Enums definieren `#[route("/")]` und teilen sich dieselbe History. Wer im Selector auf „Open Workspace" klickt, steht auf `/ws_open`; schaltet die App in diesem Moment auf den `EditorRoute`-Router um, bekommt der eine URL zu sehen, die er nicht kennt — er hat nur `/` und `/editor`. Das ist der wahrscheinlichste Weg durch die App, nicht ein Randfall. Reparieren ließe sich das mit einer erzwungenen Navigation bei jedem Wechsel. Das behandelt aber das Symptom: zwei Adressräume, die sich eine History teilen, bleiben zwei Adressräume. ### Die Struktur ```rust #[derive(Debug, Clone, Routable, PartialEq, EnumMessage)] #[rustfmt::skip] pub enum Route { #[layout(Selector)] #[route("/")] #[strum(message = "Create new Workspace")] NewWorkspace {}, #[route("/open")] #[strum(message = "Open Workspace")] OpenWorkspace {}, #[end_layout] #[nest("/workspace")] #[layout(EditorShell)] #[route("/")] #[strum(message = "Home")] Home {}, #[route("/editor")] #[strum(message = "Editor")] Editor {}, #[end_layout] #[end_nest] } ``` `App` rendert nur noch `Router::<Route> {}` — das `if workspace_selected()` entfällt ersatzlos, ebenso das `Signal<bool>`. ### Der Zustand ```rust pub struct Workspace { pub root: PathBuf, } // in App, vor dem Router: use_context_provider(|| Signal::new(None::<Workspace>)); ``` Ein benannter Typ statt `Option<PathBuf>`: `CONTEXT.md` führt „Workspace" als Domänenbegriff, der Marker bringt absehbar Einstellungen mit, die hier andocken, und ein nacktes `PathBuf` im Context zwingt jeden Leser zu der Frage, welcher Pfad gemeint ist — Workspace-Ordner, Notiz oder Konfigurationsverzeichnis. **Context, nicht Props:** Der Workspace wird im Selector gesetzt und in der `files-bar` gelesen. Beide hängen unter einem `Outlet`, und durch ein `Outlet` lassen sich keine Props durchreichen. Das ist keine Wahl, sondern eine Randbedingung. **Der Workspace-Pfad gehört nicht in die Route.** `/workspace/C%3A%5CUsers%5C...` wäre die Alternative; sie macht aus einem Dateipfad einen URL-Bestandteil samt Escaping-Fragen und gewinnt in einer Desktop-App ohne Adressleiste nichts. ### Der Guard Das `EditorShell`-Layout prüft den Context und schickt bei `None` zurück zum Selector — eine Prüfung an genau der Stelle, an der der `Outlet` steht. Der Fall ist real: Hot-Reload während der Entwicklung setzt den Context zurück, die Route bleibt bestehen; später kommt „Workspace schließen" dazu. Ein statisches `#[redirect]` scheidet aus — es sieht nur die URL, nicht den Zustand. **Beim Bauen zu prüfen:** ob die Navigation direkt im Render-Pfad erfolgen darf (`navigator().replace(...)` plus leeres `rsx!`) oder in einen `use_effect` gehört. Navigation ist ein Seiteneffekt, und Dioxus 0.7 ist an dieser Stelle nicht eindeutig dokumentiert. Die Entscheidung „Guard im Layout" ist davon unberührt, nur die Zeile darin. ### „Workspace wechseln" Braucht **keinen** strukturellen Platzhalter. Mit dem Selector unter `/` und dem Editor unter `/workspace/...` ist Wechseln eine Navigation nach `/` plus Leeren des Context — zwei Zeilen an dem Tag, an dem der Knopf dafür entsteht. --- ### Folge, die beim Ausformulieren aufgefallen ist Die Tab-Leisten werden heute aus `Route::iter()` erzeugt (`Workspace()` im Selector, `Navbar()` im Editor). Mit **einem** Enum liefert `iter()` alle vier Varianten — beide Leisten würden die Routen des jeweils anderen Bereichs mitlisten. Vorschlag, minimal und ohne Filterlogik: ```rust impl Route { pub const SELECTOR_TABS: [Self; 2] = [Self::NewWorkspace {}, Self::OpenWorkspace {}]; pub const EDITOR_TABS: [Self; 2] = [Self::Home {}, Self::Editor {}]; } ``` Die Labels bleiben über `#[strum(message = ...)]` an den Varianten, `EnumIter` wird nicht mehr gebraucht. Nachteil, offen ausgesprochen: eine neue Route zwingt einen nicht, das passende Array zu ergänzen — der Compiler prüft das nicht. Die Alternative wäre `Route::iter().filter(|r| matches!(...))`, was denselben Pflegepunkt an eine unauffälligere Stelle schiebt. Klein genug, um es beim Bauen zu entscheiden — hier festgehalten, damit es nicht überrascht.
Author
Owner

Nachtrag aus #8 (Selector-UI). Der Prototyp hat eine Annahme dieses Tickets widerlegt: Die gewonnene Variante kommt ohne Reiter aus, damit entfällt der Grund für zwei Selector-Routen.

Statt NewWorkspace {} (/) und OpenWorkspace {} (/open) gibt es eine Route:

#[route("/")]
Selector {},

Ersatzlos weg: die Konstante SELECTOR_TABS, die CSS-Klasse .workspace-selector-links, die Tab-Komponente und das #[layout(Selector)] — der Selector ist der Bildschirm, es gibt keinen Outlet-Inhalt, der umschaltet.

Der oben ausgewiesene Pflegepunkt (der Compiler erzwingt nicht, eine neue Route ins passende Tab-Array einzutragen) gilt damit nur noch für EDITOR_TABS.

Alles Übrige aus diesem Ticket bleibt gültig: ein Router, #[nest("/workspace")] für den Editor-Teil, Signal<Option<Workspace>> im Context, Guard im EditorShell-Layout.

**Nachtrag aus #8 (Selector-UI).** Der Prototyp hat eine Annahme dieses Tickets widerlegt: Die gewonnene Variante kommt ohne Reiter aus, damit entfällt der Grund für zwei Selector-Routen. Statt `NewWorkspace {}` (`/`) und `OpenWorkspace {}` (`/open`) gibt es **eine** Route: ```rust #[route("/")] Selector {}, ``` Ersatzlos weg: die Konstante `SELECTOR_TABS`, die CSS-Klasse `.workspace-selector-links`, die Tab-Komponente und das `#[layout(Selector)]` — der Selector ist der Bildschirm, es gibt keinen Outlet-Inhalt, der umschaltet. Der oben ausgewiesene Pflegepunkt (der Compiler erzwingt nicht, eine neue Route ins passende Tab-Array einzutragen) gilt damit nur noch für `EDITOR_TABS`. Alles Übrige aus diesem Ticket bleibt gültig: ein Router, `#[nest("/workspace")]` für den Editor-Teil, `Signal<Option<Workspace>>` im Context, Guard im `EditorShell`-Layout.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/dionotes-client#5