Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,34 @@
export.rs does, so `w:start="2"` makes the first page even. No harness verb
reads drawn header text, so there is no `.uit` case.

## Extension surfaces and torn-off tabs

- The suite is a library (package `docxy-suite`, lib `docxy_suite`, still
rooted at `suite/docxy/src/main.rs`) with `run(ext::Extensions)`; the
`suite` binary (`src/bin/suite.rs`) passes `Extensions::default()`, so the public build is
unchanged. A build that links an extension calls `run` itself; no
extension, edition or private repository is named here.
- `ext::Extensions::mail` is a factory (`Rc<dyn Fn(&mut App) ->
Result<Box<dyn TabSurface>, String>>`) for Mail tabs. `Surface::Ext` holds
the `TabSurface` (`view`, `focus_handle`, `title`, `set_dark`); a Mail tab
still drawn as the placeholder gets it in `attach_ext_surfaces` (render, and
File > New > Mail), and a refusal stays on the tab's status line
(`Mail: ...`), not asked again. It saves as nothing (`hot_bytes`, the
session's placeholder rule). While the tab is active the suite draws no
ribbon row (the view draws its own), its root focus goes on to the view
(`focus_ext_surface`), keys the view does not stop bubble to `route_key`,
and `ext::ShowBackstage` opens File. The theme's light/dark reaches it in
`apply_theme`.
- Tearing a tab off (`tab_tear.rs`): a chip's drag (#545) noted by
`on_drag_move` and let go away from the strip (more than `MARGIN` above or
below the chip, or out of the window) moves the tab through
`new_window_at` (New Window, #587, with the new window's top-left near the
release); a release on the strip row still reorders. The window's mouse-up
listener (`tab_tear_release`, registered with the gesture listeners) takes
that release. One tab cannot tear (New Window's refusal on the status
line). Not done: dropping a tab onto another window's strip (gpui drags do
not cross windows), a harness verb for it.

## Format plugins

- A format plugin is a `yppx_*` shared library (`.dll`/`.so`/`.dylib`)
Expand Down
2 changes: 1 addition & 1 deletion suite/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions suite/docxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
[package]
name = "docxy"
name = "docxy-suite"
description = "docxy — the doc-centric desktop suite: docs / sheets / mail in tabs, on GPUI."
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
publish = false
# src/main.rs is the library's root, not a binary (see [lib] below).
autobins = false

# The app is branded "docxy" (shown in the UI), but the produced executable is
# `suite.exe` so it never collides with the existing terminal `docxy` binary in
# the root workspace.
# The suite is a library (`run`, rooted at src/main.rs, where it has always
# lived) so a build that links an extension can start it; the public binary
# below passes none.
[lib]
name = "docxy_suite"
path = "src/main.rs"

[[bin]]
name = "suite"
path = "src/main.rs"
path = "src/bin/suite.rs"

[lints]
workspace = true
Expand Down
8 changes: 8 additions & 0 deletions suite/docxy/src/bin/suite.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! The public docxy suite: the suite (`docxy_suite`, rooted at src/main.rs)
//! with no extensions.

#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]

fn main() {
docxy_suite::run(docxy_suite::ext::Extensions::default());
}
65 changes: 65 additions & 0 deletions suite/docxy/src/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Extension surfaces: a build of the suite may link a crate that draws a
//! tab kind the public suite leaves as a placeholder (today: Mail). The
//! public `suite` binary passes [`Extensions::default`], so nothing here
//! changes it; the seam names no particular extension.
//!
//! An extension's surface is a gpui view the tab hosts as its body
//! (`Surface::Ext`). The tab owns it like any other surface, so View >
//! Window > New Window and tearing the tab off the strip carry the same view,
//! state and all, into the new window. The suite still draws the title bar,
//! the tab strip and the status line; the view draws everything below the
//! tab strip (its own ribbon included) and takes the keys while its tab is
//! the active one.

use gpui::{AnyView, App, FocusHandle, Global, actions};
use std::rc::Rc;

actions!(
docxy_ext,
[
/// Opens the suite's File backstage (New, Open, Recent, Settings).
/// An extension's own File command dispatches it from its view.
ShowBackstage
]
);

/// A tab body an extension draws.
pub trait TabSurface: 'static {
/// The view drawn as the tab's body.
fn view(&self) -> AnyView;
/// What the suite focuses when the tab becomes active, so the view's
/// own key handling sees the keys first.
fn focus_handle(&self, cx: &App) -> FocusHandle;
/// The tab's title (the folder shown, say). `None` keeps the suite's.
fn title(&self, _cx: &App) -> Option<String> {
None
}
/// The suite's theme turned light or dark (View > Theme, Auto following
/// the OS). The view may follow it.
fn set_dark(&self, _dark: bool, _cx: &mut App) {}
}

/// Makes the surface of a new tab of one kind. An error is said on the tab's
/// status line and the tab keeps the placeholder.
pub type Factory = Rc<dyn Fn(&mut App) -> Result<Box<dyn TabSurface>, String>>;

/// What a build links in. Every field is optional.
#[derive(Default)]
pub struct Extensions {
/// Draws Mail tabs (File > New > Mail, a restored Mail tab).
pub mail: Option<Factory>,
}

struct Installed(Extensions);

impl Global for Installed {}

pub(crate) fn install(cx: &mut App, extensions: Extensions) {
cx.set_global(Installed(extensions));
}

/// A new Mail surface, when an extension draws Mail: `None` without one.
pub(crate) fn new_mail(cx: &mut App) -> Option<Result<Box<dyn TabSurface>, String>> {
let factory = cx.try_global::<Installed>()?.0.mail.clone()?;
Some(factory(cx))
}
Loading
Loading