Ely Office: six offline-first desktop apps in Rust and GPUI

ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
//! The import dialog: encoding, separator, quote, column kinds.
use ely_gpui_component::{
buttons::{Button, ButtonVariant, SegmentedControl},
data_display::DescriptionList,
feedback::InlineMessage,
forms::{Choice, Select},
layout::ScrollArea,
overlays::Dialog as Card,
primitives::Severity,
theme::{ActiveTheme as _, ControlSize},
};
use ely_lok::sheet::csv::{COLUMNS, Column, ENCODINGS};
use gpui::{App, Entity, ParentElement as _, SharedString, Styled as _, Window, div, rems};
use super::{Pending, SEPARATORS};
use crate::book::Book;
use crate::dialog::{Dialog, edit};
fn pending_of(dialog: &mut Dialog) -> &mut Pending {
match dialog {
Dialog::Import(pending) => pending,
_ => unreachable!("an import dialog"),
}
}
/// Changes a choice that reads the file anew.
fn reread(this: &Entity<Book>, cx: &mut App, change: impl FnOnce(&mut Pending)) {
edit(this, cx, |dialog, _, _| change(pending_of(dialog)));
this.update(cx, |book, cx| book.scan_import(cx));
}
fn column_key(column: Column) -> &'static str {
match column {
Column::Standard => "table.import.standard",
Column::Text => "table.import.text",
Column::DateMdy => "table.import.mdy",
Column::DateDmy => "table.import.dmy",
Column::DateYmd => "table.import.ymd",
Column::Skip => "table.import.skip",
}
}
fn separator_key(separator: char) -> &'static str {
match separator {
',' => "table.import.comma",
';' => "table.import.semicolon",
'\t' => "table.import.tab",
'|' => "table.import.pipe",
_ => "table.import.space",
}
}
/// One column: its kind above its first cells.
fn column(this: &Entity<Book>, at: usize, kind: Column, rows: &[Vec<String>], cx: &App) -> gpui::Div {
let label = |key: &str| Book::text(cx, key, &[]);
let picking = this.clone();
let kinds = Select::new(
("table-import-kind", at),
COLUMNS
.iter()
.enumerate()
.map(|(index, kind)| Choice::new(index.to_string(), label(column_key(*kind)))),
)
.label(Book::text(cx, "table.import.kind_of", &[("n", &(at + 1).to_string())]))
.selected(COLUMNS.iter().position(|each| *each == kind).expect("listed").to_string())
.on_change(move |value, _, cx| {
let Ok(index) = value.parse::<usize>() else {
return;
};
edit(&picking, cx, |dialog, _, _| {
pending_of(dialog).how.columns[at] = COLUMNS[index];
})
});
let fg = cx.theme().colors.fg;
let cells = rows.iter().map(|row| {
div()
.h_6()
.truncate()
.text_color(fg)
.child(SharedString::from(row.get(at).cloned().unwrap_or_default()))
});
div().w_32().flex_none().flex().flex_col().gap_1().child(kinds).children(cells)
}
pub fn import_dialog(
this: Entity<Book>,
pending: &Pending,
dismiss: impl Fn(&mut Window, &mut App) + 'static,
cx: &App,
) -> Card {
let label = |key: &str| Book::text(cx, key, &[]);
let how = &pending.how;
let picking = this.clone();
let encoding = Select::new(
"table-import-encoding",
ENCODINGS
.iter()
.enumerate()
.map(|(index, encoding)| Choice::new(index.to_string(), encoding.label())),
)
.label(label("table.import.encoding"))
.selected(ENCODINGS.iter().position(|e| *e == how.encoding).expect("listed").to_string())
.on_change(move |value, _, cx| {
if let Ok(index) = value.parse::<usize>() {
reread(&picking, cx, |pending| pending.how.encoding = ENCODINGS[index]);
}
});
let picking = this.clone();
let separator = Select::new(
"table-import-separator",
SEPARATORS
.iter()
.map(|each| Choice::new(each.to_string(), label(separator_key(*each)))),
)
.label(label("table.import.separator"))
.selected(how.separator.to_string())
.on_change(move |value, _, cx| {
if let Some(separator) = value.chars().next() {
reread(&picking, cx, |pending| pending.how.separator = separator);
}
});
let picking = this.clone();
let quoted = how.quote.map_or(String::new(), String::from);
let quote = SegmentedControl::new("table-import-quote", quoted)
.size(ControlSize::Sm)
.segment("\"", "\"", None)
.segment("'", "'", None)
.segment("", label("table.import.no_quote"), None)
.on_change(move |value, _, cx| {
let quote = value.chars().next();
reread(&picking, cx, |pending| pending.how.quote = quote);
});
let choices = DescriptionList::new()
.item(label("table.import.encoding"), div().w_full().max_w_48().child(encoding))
.item(label("table.import.separator"), div().w_full().max_w_48().child(separator))
.item(label("table.import.quote"), quote);
let columns = how
.columns
.iter()
.enumerate()
.map(|(at, kind)| column(&this, at, *kind, &pending.rows, cx));
// As wide as its columns, so it scrolls.
let shown = how.columns.len() as f32;
let row = div().w(rems(shown * 8.5)).flex().gap_2().pb_2().children(columns);
let preview = ScrollArea::new("table-import-preview").h_64().child(row);
let broken = pending
.broken
.then(|| InlineMessage::new(Severity::Danger, label("table.import.broken")));
let off = pending.reading || pending.broken;
let name = ely_office::files::file_name(&pending.path);
let go = label("table.import.go");
Card::new(
"table-import",
Book::text(cx, "table.import.title", &[("name", &name)]),
dismiss,
)
.child(choices)
.children(broken)
.child(preview)
.action(ely_office::cancel(cx))
.action(move |_| {
Button::new("table-import-go", go)
.variant(ButtonVariant::Primary)
.disabled(off)
.on_click(move |_, _, cx| this.update(cx, |book, cx| book.import_asked(cx)))
})
}