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:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,379 @@
|
||||
//! The selected objects' panel (SHOW-03 to 06).
|
||||
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
data_display::{PropertyGrid, PropertyGroup},
|
||||
forms::{NumberInput, Switch},
|
||||
primitives::IconName,
|
||||
theme::ControlSize,
|
||||
};
|
||||
use ely_lok::show::Selection;
|
||||
use ely_office::panel::{self as kernel, palette, row};
|
||||
use ely_office::prelude::*;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, IntoElement, ParentElement as _, SharedString, Styled as _,
|
||||
Window, div,
|
||||
};
|
||||
|
||||
use crate::deck::Deck;
|
||||
use crate::design::{Panel, on_deck};
|
||||
|
||||
/// Centimetres: 1/100 mm per cm, step, places.
|
||||
const CM: Unit = (1000.0, 0.1, 2, twips);
|
||||
/// The swatches' clear buttons.
|
||||
pub(crate) const NO_FILL: kernel::Clear = ("show.object.no_fill", "object.fill_none", "0");
|
||||
pub(crate) const NO_LINE: kernel::Clear = ("show.object.no_line", "object.line_none", "0");
|
||||
/// 1/100 mm in a point.
|
||||
const POINT: f64 = 2540.0 / 72.0;
|
||||
|
||||
/// Shown units: scale, step, places, then what the kernel takes.
|
||||
type Unit = (f64, f64, usize, fn(i64) -> i64);
|
||||
|
||||
/// LOK reads transform arguments as twips.
|
||||
pub(crate) fn twips(mm100: i64) -> i64 {
|
||||
(mm100 * 72 + 63).div_euclid(127)
|
||||
}
|
||||
|
||||
fn same(value: i64) -> i64 {
|
||||
value
|
||||
}
|
||||
|
||||
impl kernel::Panel for Panel {
|
||||
fn state(&self, uno: &str, cx: &App) -> Option<SharedString> {
|
||||
let deck = self.deck.upgrade()?;
|
||||
let tab = deck.read(cx).active_tab()?;
|
||||
let view = tab.view.read(cx);
|
||||
view.state(uno).map(|state| state.to_string().into())
|
||||
}
|
||||
|
||||
fn run(&mut self, id: &str, values: &[&str], cx: &mut Context<Self>) {
|
||||
on_deck(&self.deck, cx, |deck, cx| {
|
||||
deck.object_command(id, values, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends `value` unless it is there or on its way.
|
||||
pub(crate) fn commit(
|
||||
this: &Entity<Panel>,
|
||||
field: impl Into<String>,
|
||||
(value, now): (i64, Option<i64>),
|
||||
send: impl FnOnce(&mut Deck, &mut Context<Deck>) + 'static,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let field = field.into();
|
||||
this.update(cx, |panel, cx| {
|
||||
let read = panel.deck.upgrade().and_then(|deck| {
|
||||
deck.read(cx)
|
||||
.active_tab()
|
||||
.map(|tab| (tab.id, tab.extra.reads))
|
||||
});
|
||||
if panel.seen != read {
|
||||
panel.sent.clear();
|
||||
panel.seen = read;
|
||||
}
|
||||
let mark = (value, now);
|
||||
if now == Some(value) || panel.sent.get(&field) == Some(&mark) {
|
||||
log::info!("show: {field} already holds {value}");
|
||||
return;
|
||||
}
|
||||
panel.sent.insert(field, mark);
|
||||
on_deck(&panel.deck, cx, send);
|
||||
});
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
/// Groups for the selection on the shown slide.
|
||||
pub(crate) fn object_grid(
|
||||
&self,
|
||||
selection: &Selection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> PropertyGrid {
|
||||
let mut grid = PropertyGrid::new("show-object").group(self.geometry(selection, cx));
|
||||
let kind = selection.kind.as_str();
|
||||
// Lines and pictures have an outline but no fill.
|
||||
let fills = !matches!(kind, "image" | "line" | "connector");
|
||||
grid = grid.group(self.look(selection, fills, cx));
|
||||
if matches!(kind, "text" | "shape" | "mixed") {
|
||||
grid = grid.group(self.words(selection, window, cx));
|
||||
}
|
||||
if kind == "image" {
|
||||
grid = grid.group(self.picture(cx));
|
||||
}
|
||||
if kind == "table" {
|
||||
grid = grid.group(self.table_group(cx));
|
||||
}
|
||||
if kind == "chart" {
|
||||
grid = grid.group(self.chart_group(cx));
|
||||
}
|
||||
if let Some(group) = (kind == "media").then(|| self.media_group(cx)).flatten() {
|
||||
grid = grid.group(group);
|
||||
}
|
||||
grid.group(self.arrange(selection, cx))
|
||||
}
|
||||
|
||||
/// A number field: kernel units over `scale`, `step`, `places`.
|
||||
fn field(
|
||||
&self,
|
||||
(field, command): (&'static str, &'static str),
|
||||
(now, (scale, step, places, kernel)): (Option<i64>, Unit),
|
||||
range: (f64, f64),
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let this = cx.entity();
|
||||
let label = Deck::text(cx, &format!("show.object.{field}"), &[]);
|
||||
let id = SharedString::from(format!("show-{field}"));
|
||||
let mut input = NumberInput::new(id, now.unwrap_or(0) as f64 / scale)
|
||||
.label(label)
|
||||
.range(range.0, range.1)
|
||||
.step(step)
|
||||
.precision(places)
|
||||
.size(ControlSize::Sm)
|
||||
.on_commit(move |shown, _, cx| {
|
||||
let value = (shown * scale).round() as i64;
|
||||
let send = move |deck: &mut Deck, cx: &mut Context<Deck>| {
|
||||
deck.object_command(command, &[&kernel(value).to_string()], cx)
|
||||
};
|
||||
commit(&this, field, (value, now), send, cx)
|
||||
});
|
||||
if now.is_none() {
|
||||
input = input.blank(Deck::text(cx, "office.props.mixed", &[]));
|
||||
}
|
||||
div().w_full().max_w_32().child(input).into_any_element()
|
||||
}
|
||||
|
||||
fn geometry(&self, sel: &Selection, cx: &mut Context<Self>) -> PropertyGroup {
|
||||
let (x, y, w, h) = sel.rect;
|
||||
let (cm, at, long) = (CM, (-300.0, 300.0), (0.01, 300.0));
|
||||
let place = row([
|
||||
self.field(("x", "object.x"), (Some(x), cm), at, cx),
|
||||
self.field(("y", "object.y"), (Some(y), cm), at, cx),
|
||||
]);
|
||||
let extent = row([
|
||||
self.field(("width", "object.width"), (Some(w), cm), long, cx),
|
||||
self.field(("height", "object.height"), (Some(h), cm), long, cx),
|
||||
]);
|
||||
let this = cx.entity();
|
||||
let (now, center) = (sel.rotation, sel.rotation_center);
|
||||
let label = Deck::text(cx, "show.object.rotation", &[]);
|
||||
let turn = NumberInput::new("show-rotation", now as f64 / 100.0)
|
||||
.label(label)
|
||||
.range(0.0, 359.9)
|
||||
.step(1.0)
|
||||
.precision(1)
|
||||
.size(ControlSize::Sm)
|
||||
.on_commit(move |degrees, _, cx| {
|
||||
let angle = (degrees * 100.0).round() as i64 % 36_000;
|
||||
let values = [angle, twips(center.0), twips(center.1)].map(|n| n.to_string());
|
||||
commit(
|
||||
&this,
|
||||
"rotation",
|
||||
(angle, Some(now)),
|
||||
move |deck, cx| {
|
||||
let values: Vec<&str> = values.iter().map(String::as_str).collect();
|
||||
deck.object_command("object.rotate", &values, cx)
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let locked = sel.lock_position == Some(true);
|
||||
let lock = Switch::new("show-lock", locked)
|
||||
.name(Deck::text(cx, "show.object.lock", &[]))
|
||||
.on_change({
|
||||
let deck = self.deck.clone();
|
||||
move |on, _, cx| {
|
||||
let on = on.to_string();
|
||||
on_deck(&deck, cx, |deck, cx| {
|
||||
deck.object_command("object.lock", &[&on, &on], cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
let tip = |key: &str| Deck::text(cx, key, &[]);
|
||||
PropertyGroup::new(tip("show.object.geometry"))
|
||||
.row(tip("show.object.position"), place)
|
||||
.row(tip("show.object.size"), extent)
|
||||
.row(tip("show.object.rotation"), div().w_full().max_w_32().child(turn))
|
||||
.row(tip("show.object.lock"), lock)
|
||||
}
|
||||
|
||||
fn look(&self, sel: &Selection, fills: bool, cx: &mut Context<Self>) -> PropertyGroup {
|
||||
let this = cx.entity();
|
||||
let fill = palette(
|
||||
&this,
|
||||
"object.fill",
|
||||
(sel.fill, "show.object.no_fill"),
|
||||
Some(NO_FILL),
|
||||
cx,
|
||||
);
|
||||
let line = palette(
|
||||
&this,
|
||||
"object.line_color",
|
||||
(sel.line, "show.object.no_line"),
|
||||
Some(NO_LINE),
|
||||
cx,
|
||||
);
|
||||
let see = self.field(
|
||||
("transparency", "object.transparency"),
|
||||
(sel.transparency, (1.0, 1.0, 0, same)),
|
||||
(0.0, 100.0),
|
||||
cx,
|
||||
);
|
||||
let width = self.field(
|
||||
("line_width", "object.line_width"),
|
||||
(sel.line_width, (POINT, 0.25, 2, same)),
|
||||
(0.0, 20.0),
|
||||
cx,
|
||||
);
|
||||
let shadow = Switch::new("show-shadow", sel.shadow == Some(true))
|
||||
.name(Deck::text(cx, "show.object.shadow", &[]))
|
||||
.on_change({
|
||||
let deck = self.deck.clone();
|
||||
move |on, _, cx| {
|
||||
let on = on.to_string();
|
||||
on_deck(&deck, cx, |deck, cx| {
|
||||
deck.object_command("object.shadow", &[&on], cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
let tip = |key: &str| Deck::text(cx, key, &[]);
|
||||
let title = if fills {
|
||||
"show.object.look"
|
||||
} else {
|
||||
"show.object.line"
|
||||
};
|
||||
let mut group = PropertyGroup::new(tip(title));
|
||||
if fills {
|
||||
group = group
|
||||
.row(tip("show.object.fill"), fill)
|
||||
.row(tip("show.object.transparency"), see)
|
||||
.row(tip("show.object.shadow"), shadow);
|
||||
}
|
||||
group
|
||||
.row(tip("show.object.line"), line)
|
||||
.row(tip("show.object.line_width"), width)
|
||||
}
|
||||
|
||||
fn chart_group(&self, cx: &mut Context<Self>) -> PropertyGroup {
|
||||
let deck = self.deck.clone();
|
||||
let change = Button::new("show-chart-edit", Deck::text(cx, "show.chart.edit", &[]))
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, _, cx| on_deck(&deck, cx, |deck, cx| deck.ask_chart(cx)));
|
||||
let name = Deck::text(cx, "show.kind.chart", &[]);
|
||||
PropertyGroup::new(name.clone()).row(name, change)
|
||||
}
|
||||
|
||||
fn picture(&self, cx: &mut Context<Self>) -> PropertyGroup {
|
||||
let deck = self.deck.clone();
|
||||
let replace = Button::new("show-replace", Deck::text(cx, "show.object.replace", &[]))
|
||||
.size(ControlSize::Sm)
|
||||
.on_click(move |_, _, cx| on_deck(&deck, cx, |deck, cx| deck.pick_picture(true, cx)));
|
||||
let crop = kernel::button(
|
||||
self,
|
||||
"image.crop",
|
||||
"show.object.crop",
|
||||
"show.object.why_picture",
|
||||
cx,
|
||||
);
|
||||
PropertyGroup::new(Deck::text(cx, "show.object.picture", &[])).row(
|
||||
Deck::text(cx, "show.object.picture", &[]),
|
||||
row([replace.into_any_element(), crop]),
|
||||
)
|
||||
}
|
||||
|
||||
fn arrange(&self, sel: &Selection, cx: &mut Context<Self>) -> PropertyGroup {
|
||||
let act = |id: &'static str, (icon, key): (Option<IconName>, &str), on: bool| {
|
||||
let deck = self.deck.clone();
|
||||
let run = move |_: &gpui::ClickEvent, _: &mut Window, cx: &mut App| {
|
||||
on_deck(&deck, cx, |deck, cx| deck.object_command(id, &[], cx))
|
||||
};
|
||||
let label = Deck::text(cx, key, &[]);
|
||||
match icon {
|
||||
Some(icon) => IconButton::new(id, icon)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip(label)
|
||||
.disabled(!on)
|
||||
.on_click(run)
|
||||
.into_any_element(),
|
||||
None => Button::new(id, label)
|
||||
.size(ControlSize::Sm)
|
||||
.disabled(!on)
|
||||
.on_click(run)
|
||||
.into_any_element(),
|
||||
}
|
||||
};
|
||||
let many = sel.count >= 3;
|
||||
let order = row([
|
||||
act("image.front", (None, "show.object.front"), true),
|
||||
act("image.forward", (None, "show.object.forward"), true),
|
||||
act("image.backward", (None, "show.object.backward"), true),
|
||||
act("image.back", (None, "show.object.back"), true),
|
||||
]);
|
||||
let icon = |icon, key| (Some(icon), key);
|
||||
let align = row([
|
||||
act(
|
||||
"object.align_left",
|
||||
icon(IconName::AlignStartVertical, "show.object.align_left"),
|
||||
true,
|
||||
),
|
||||
act(
|
||||
"object.align_center",
|
||||
icon(IconName::AlignCenterVertical, "show.object.align_center"),
|
||||
true,
|
||||
),
|
||||
act(
|
||||
"object.align_right",
|
||||
icon(IconName::AlignEndVertical, "show.object.align_right"),
|
||||
true,
|
||||
),
|
||||
act(
|
||||
"object.align_top",
|
||||
icon(IconName::AlignStartHorizontal, "show.object.align_top"),
|
||||
true,
|
||||
),
|
||||
act(
|
||||
"object.align_middle",
|
||||
icon(IconName::AlignCenterHorizontal, "show.object.align_middle"),
|
||||
true,
|
||||
),
|
||||
act(
|
||||
"object.align_bottom",
|
||||
icon(IconName::AlignEndHorizontal, "show.object.align_bottom"),
|
||||
true,
|
||||
),
|
||||
]);
|
||||
let spread = row([
|
||||
act(
|
||||
"object.spread_across",
|
||||
icon(
|
||||
IconName::AlignHorizontalDistributeCenter,
|
||||
"show.object.spread_across",
|
||||
),
|
||||
many,
|
||||
),
|
||||
act(
|
||||
"object.spread_down",
|
||||
icon(
|
||||
IconName::AlignVerticalDistributeCenter,
|
||||
"show.object.spread_down",
|
||||
),
|
||||
many,
|
||||
),
|
||||
]);
|
||||
let group = row([
|
||||
act("object.group", (None, "show.object.group"), sel.can_group),
|
||||
act(
|
||||
"object.ungroup",
|
||||
(None, "show.object.ungroup"),
|
||||
sel.can_ungroup,
|
||||
),
|
||||
]);
|
||||
let tip = |key: &str| Deck::text(cx, key, &[]);
|
||||
PropertyGroup::new(tip("show.object.arrange"))
|
||||
.row(tip("show.object.order"), order)
|
||||
.row(tip("show.object.align_to"), align)
|
||||
.row(tip("show.object.spread"), spread)
|
||||
.row(tip("show.object.grouping"), group)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user