Files
ElyOffice/apps/ely-show/src/object.rs
T
ZacharyZhang-NY b7fca0146d 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.
2026-10-08 21:20:08 -04:00

380 lines
14 KiB
Rust

//! 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)
}
}