Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6917984329 |
No files matched your search
@@ -45,8 +45,3 @@ sync_github_name = false
|
||||
real_name = "Ahmed Moussa"
|
||||
display_name = "Ahmed Moussa"
|
||||
sync_github_name = true
|
||||
|
||||
[people.vbasky]
|
||||
real_name = "Vikram Bhaskaran"
|
||||
display_name = "Vikram Bhaskaran"
|
||||
sync_github_name = false
|
||||
@@ -246,8 +246,9 @@ fn candidates(s: &Session, f: impl Fn(&Node) -> bool) -> Result<Vec<NodeId>> {
|
||||
|
||||
fn finish_same(s: &mut Session, cmd: &str, ids: Vec<NodeId>) -> Result<Value> {
|
||||
let n = ids.len();
|
||||
let from = s.doc()?.selection.objects.clone();
|
||||
s.select(|_, sel| sel.set(ids))?;
|
||||
s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), json!({})));
|
||||
s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), json!({}), from));
|
||||
Ok(json!({ "count": n }))
|
||||
}
|
||||
|
||||
|
||||
@@ -216,8 +216,20 @@ fn none(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn reselect(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let Some((c, p)) = s.doc()?.last_selection_cmd.clone() else { return ok() };
|
||||
s.execute(&c, &p)
|
||||
let st = s.doc()?;
|
||||
let Some((c, p, from)) = st.last_selection_cmd.clone() else { return ok() };
|
||||
// After Deselect, a Same command takes its reference again from the objects that were selected
|
||||
// when it ran, those still there (#903).
|
||||
let from: Vec<NodeId> = from.into_iter().filter(|id| st.doc.node(*id).is_some()).collect();
|
||||
let restore = st.selection.objects.is_empty() && !from.is_empty();
|
||||
if restore {
|
||||
s.select(|_, sel| sel.set(from))?;
|
||||
}
|
||||
let r = s.execute(&c, &p);
|
||||
if restore && r.is_err() {
|
||||
s.select(|_, sel| sel.clear())?;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
fn inverse(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
@@ -401,8 +413,9 @@ fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &No
|
||||
for l in st.doc.layers.iter().filter(|l| l.visible && !l.locked) {
|
||||
visit(&st.doc, l, &f, &mut ids);
|
||||
}
|
||||
let from = st.selection.objects.clone();
|
||||
s.select(|_, sel| sel.set(ids.iter().copied()))?;
|
||||
s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone()));
|
||||
s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone(), from));
|
||||
Ok(json!({ "count": ids.len() }))
|
||||
}
|
||||
|
||||
|
||||
@@ -126,8 +126,9 @@ pub struct DocState {
|
||||
pub undo_group: Option<UndoGroup>,
|
||||
/// For Object → Transform → Transform Again (⌘D).
|
||||
pub last_transform: Option<(Affine, bool)>,
|
||||
/// Selection saved by Select → Reselect.
|
||||
pub last_selection_cmd: Option<(String, Value)>,
|
||||
/// What Select → Reselect repeats: the last selection command, its params and the objects
|
||||
/// selected when it ran (a Same command's reference objects).
|
||||
pub last_selection_cmd: Option<(String, Value, Vec<NodeId>)>,
|
||||
/// Process-unique id of this open document (tab indices shift when tabs close).
|
||||
pub uid: u64,
|
||||
/// View Opacity Mask (Alt-click the mask thumbnail): the masked object whose mask the canvas
|
||||
|
||||
@@ -149,6 +149,18 @@ fn select_same_fill() {
|
||||
let r = s.execute("select.same.fillColor", &json!({})).unwrap();
|
||||
assert_eq!(r["count"], 2);
|
||||
assert!(!s.doc().unwrap().selection.contains(a));
|
||||
// Reselect after Deselect repeats it from the same reference object (#903).
|
||||
let blue = s.doc().unwrap().selection.objects.clone();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.execute("select.reselect", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects, blue);
|
||||
// With its reference object gone, it fails as Select Same does, and selects nothing.
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
for id in &blue {
|
||||
s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap();
|
||||
}
|
||||
assert!(s.execute("select.reselect", &json!({})).is_err());
|
||||
assert!(s.doc().unwrap().selection.objects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -187,10 +187,37 @@ pub(crate) fn typed_unit(ctx: &egui::Context, unit: Unit) -> Unit {
|
||||
/// without a unit is in `unit` ([`typed_unit`]: in points in a picas field with Numbers Without
|
||||
/// Units Are Points on).
|
||||
pub fn num_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option<f64>, unit: Unit, width: f32) -> Option<f64> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let id = ui.id().with(id);
|
||||
let shown = value.map(|v| unit.format(v)).unwrap_or_default();
|
||||
take_dialog_focus(ui, id, &shown);
|
||||
let (mut buf, resp, rect) = recessed_text(ui, id, &shown, width, 1);
|
||||
let mut buf: String = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| shown.clone());
|
||||
let editing = ui.memory(|m| m.has_focus(id));
|
||||
if !editing {
|
||||
buf = shown.clone();
|
||||
}
|
||||
take_dialog_focus(ui, id, &buf);
|
||||
let (rect, resp) = ui
|
||||
.allocate_ui_with_layout(vec2(width, 26.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.set_min_width(width);
|
||||
let framed = egui::Frame::NONE
|
||||
.fill(t.input)
|
||||
.stroke(Stroke::new(1.0, if editing { t.accent } else { t.input_border }))
|
||||
.corner_radius(CornerRadius::same(2))
|
||||
.inner_margin(egui::Margin::symmetric(6, 4))
|
||||
.show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut buf)
|
||||
.id(id)
|
||||
.frame(egui::Frame::NONE)
|
||||
.desired_width(width - 14.0)
|
||||
.min_size(vec2(width - 14.0, 0.0))
|
||||
.font(egui::FontId::proportional(12.5))
|
||||
.text_color(t.text_strong),
|
||||
)
|
||||
});
|
||||
(framed.response.rect, framed.inner)
|
||||
})
|
||||
.inner;
|
||||
select_all_on_focus(ui, &resp, &buf);
|
||||
// ↑/↓ and the wheel step in the field's unit; a drag on its label scrubs it.
|
||||
let read = typed_unit(ui.ctx(), unit);
|
||||
@@ -322,32 +349,27 @@ fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let editing = ui.memory(|m| m.has_focus(id));
|
||||
let mut buf: String = if editing { ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| shown.to_string()) } else { shown.to_string() };
|
||||
// The box is exactly as tall as the other controls of a row (26 points a line), its text
|
||||
// centred in it: a frame around the text came out a point taller and stood out next to
|
||||
// spinners and dropdowns (#900).
|
||||
let height = 26.0 + 16.0 * (rows.max(1) - 1) as f32;
|
||||
let layout = if rows > 1 { egui::Layout::top_down(egui::Align::Min) } else { egui::Layout::left_to_right(egui::Align::Center) };
|
||||
let (rect, resp) = ui
|
||||
.allocate_ui_with_layout(vec2(width, height), layout, |ui| {
|
||||
ui.set_min_size(vec2(width, height));
|
||||
let rect = ui.max_rect();
|
||||
ui.painter().rect(rect, 2.0, t.input, Stroke::new(1.0, if editing { t.accent } else { t.input_border }), StrokeKind::Inside);
|
||||
let margin = egui::Margin { left: 7, right: 7, top: if rows > 1 { 4 } else { 0 }, bottom: 0 };
|
||||
let resp = egui::Frame::NONE
|
||||
.inner_margin(margin)
|
||||
.allocate_ui_with_layout(vec2(width, height), egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.set_min_width(width);
|
||||
let framed = egui::Frame::NONE
|
||||
.fill(t.input)
|
||||
.stroke(Stroke::new(1.0, if editing { t.accent } else { t.input_border }))
|
||||
.corner_radius(CornerRadius::same(2))
|
||||
.inner_margin(egui::Margin::symmetric(6, 4))
|
||||
.show(ui, |ui| {
|
||||
let edit = if rows > 1 { egui::TextEdit::multiline(&mut buf).desired_rows(rows) } else { egui::TextEdit::singleline(&mut buf) };
|
||||
ui.add(
|
||||
edit.id(id)
|
||||
.frame(egui::Frame::NONE)
|
||||
.margin(egui::Margin::ZERO)
|
||||
.desired_width(width - 14.0)
|
||||
.min_size(vec2(width - 14.0, 0.0))
|
||||
.font(egui::FontId::proportional(12.5))
|
||||
.text_color(t.text_strong),
|
||||
)
|
||||
})
|
||||
.inner;
|
||||
(rect, resp)
|
||||
});
|
||||
(framed.response.rect, framed.inner)
|
||||
})
|
||||
.inner;
|
||||
ui.data_mut(|d| d.insert_temp(id, buf.clone()));
|
||||
@@ -1104,8 +1126,7 @@ fn spin_generic(
|
||||
let h = 26.0;
|
||||
let enabled = ui.is_enabled();
|
||||
ui.scope(|ui| {
|
||||
// The spinner, the field and the chevron share their borders: one box in three parts.
|
||||
ui.spacing_mut().item_spacing.x = -1.0;
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let (sr, sresp) = ui.allocate_exact_size(vec2(16.0, h), Sense::click());
|
||||
ui.painter().rect_filled(sr, CornerRadius { nw: 2, sw: 2, ne: 0, se: 0 }, t.input);
|
||||
ui.painter().rect_stroke(sr, CornerRadius { nw: 2, sw: 2, ne: 0, se: 0 }, Stroke::new(1.0, t.input_border), StrokeKind::Inside);
|
||||
@@ -1133,7 +1154,7 @@ fn spin_generic(
|
||||
let nv = if up.contains(p) { v + step } else { v - step };
|
||||
out = Some(SpinPick::Value(nv.max(min)));
|
||||
}
|
||||
let fw = if presets.is_empty() { width - 15.0 } else { width - 34.0 };
|
||||
let fw = if presets.is_empty() { width - 16.0 } else { width - 36.0 };
|
||||
if let Some(v) = field(ui, fw) {
|
||||
out = Some(SpinPick::Value(v.max(min)));
|
||||
}
|
||||
@@ -1771,9 +1792,8 @@ fn chevron_cell(ui: &mut Ui, h: f32) -> Response {
|
||||
pub fn text_presets(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: &str, presets: &[&str], width: f32) -> Option<String> {
|
||||
let mut out = None;
|
||||
ui.scope(|ui| {
|
||||
// The field and the chevron share a border.
|
||||
ui.spacing_mut().item_spacing.x = -1.0;
|
||||
out = text_field(ui, &id, Some(value), width - 19.0, 1);
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
out = text_field(ui, &id, Some(value), width - 20.0, 1);
|
||||
let resp = chevron_cell(ui, 26.0);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(width - 10.0);
|
||||
|
||||
Reference in new issue
Block a user