mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
Merge PR #1504: Color panel edits the gradient stop selected in the Gradient panel
This commit is contained in:
@@ -456,6 +456,7 @@ Newest first.
|
||||
- **2026-10-16** · IDML: a page keeps column guides that were dragged off centre (InDesign's Custom columns). They were divided evenly on import, so the gutter sprang back to the middle; the imported positions now place the columns, with the canvas guides, snapping and text placement following them, and they are written back on export. Layout › Margins and Columns re-divides them evenly when the count or gutter changes, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-16** · IDML: custom column positions are checked before they are used (#1096). A set must give two edges per column, ascending from the left margin to the right one; any other (a wrong count, unsorted or non-numeric values, a list longer than 216 columns allow, or positions left behind by a margin or page-size change) is the even split, in the canvas, on import and on export, instead of columns drawn outside the margins. No readiness percentage change.
|
||||
- **2026-10-21** · Swatches panel rows show InDesign's columns, drawn in code: a crossed pencil on the swatches that can't be edited ([None], [Registration], [Black]), the colour type (process or spot) and the colour mode (CMYK, RGB, Gray, or [None]'s and [Registration]'s own glyphs), in place of a lock and "CMYK" text. Tint reads 100 with nothing selected. No readiness percentage change.
|
||||
- **2026-10-21** · With a gradient fill or stroke, the Color panel shows the stop selected in the Gradient panel (the stop before a selected midpoint) with its values, and its C/M/Y/K or R/G/B sliders and spectrum colour that stop, keeping the others, as in InDesign. It showed "[None] — choose a colour on the ramp", and a spectrum click replaced the whole gradient with a flat colour. Found in the Learn Skills Daily course, chapter 24. No readiness percentage change.
|
||||
- **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
|
||||
- **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
|
||||
- **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
|
||||
|
||||
@@ -81,3 +81,42 @@ fn a_typed_tint_is_kept_while_typing_and_applied_on_enter_or_leaving_the_field()
|
||||
h.run_steps(3);
|
||||
assert_eq!(fill_tint(h.state(), id), 0.0, "dragging the slider to the left end applies 0 %");
|
||||
}
|
||||
|
||||
/// The course's chapter 24: with a gradient fill, the Color panel shows the stop selected in the
|
||||
/// Gradient panel (black, then) and its C/M/Y/K fields colour that stop, keeping the others.
|
||||
#[test]
|
||||
fn the_color_panel_edits_the_gradient_stop_selected_in_the_gradient_panel() {
|
||||
use super::swatches::{GRAD_SEL, RampSel};
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let id = app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
// InDesign's default White → Black.
|
||||
app.run("object.gradient", json!({})).unwrap();
|
||||
let mut h = harness(app);
|
||||
h.run_steps(2);
|
||||
h.ctx.data_mut(|d| d.insert_temp(egui::Id::unique(GRAD_SEL), RampSel::Stop(1)));
|
||||
h.run_steps(3);
|
||||
assert!(h.query_by_label("R=0 G=0 B=0").is_some(), "the black stop's values");
|
||||
// C: the topmost field.
|
||||
let at = h
|
||||
.query_all_by(|n| matches!(n.role(), Role::SpinButton | Role::TextInput))
|
||||
.map(|n| n.rect())
|
||||
.min_by(|a, b| a.min.y.total_cmp(&b.min.y))
|
||||
.unwrap()
|
||||
.center();
|
||||
h.hover_at(at);
|
||||
h.drag_at(at);
|
||||
h.drop_at(at);
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Text("100".into()));
|
||||
h.run_steps(2);
|
||||
h.key_press(egui::Key::Enter);
|
||||
h.run_steps(3);
|
||||
let doc = h.state().session.doc().unwrap().doc.clone();
|
||||
let fill = &doc.item(designcraft_doc::ItemId(id)).unwrap().fill.swatch;
|
||||
let g = designcraft_color::swatch::resolve_gradient(&doc.swatches, fill).cloned().expect("still a gradient");
|
||||
assert_eq!(g.stops[1].color, designcraft_color::Color::cmyk(1.0, 0.0, 0.0, 1.0), "C 100 on the black stop");
|
||||
assert_eq!(g.stops[0].color, designcraft_color::Gradient::default().stops[0].color, "the white stop stays");
|
||||
}
|
||||
|
||||
@@ -570,6 +570,11 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
app.ui.stroke_front = stroke;
|
||||
ui.data_mut(|d| d.insert_temp(mode_id, mode));
|
||||
// A gradient: the stop selected in the Gradient panel, as InDesign's Color panel shows it.
|
||||
if !text && let Some(g) = designcraft_color::swatch::resolve_gradient(&doc.swatches, &name).cloned() {
|
||||
gradient_stop(app, ui, &g, target, mode);
|
||||
return;
|
||||
}
|
||||
let sw = doc.swatch(&name).cloned();
|
||||
let Some(c) = doc.resolve_color(&name, 1.0) else {
|
||||
ui.label(crate::rtl::widget(
|
||||
@@ -722,8 +727,80 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 {
|
||||
egui::Color32::from_rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
}
|
||||
|
||||
/// The Color panel on a gradient fill (or stroke): the stop selected in the Gradient panel (the
|
||||
/// stop before a selected midpoint), in the panel's colour mode, as InDesign's. Its fields and the
|
||||
/// spectrum colour that stop.
|
||||
fn gradient_stop(app: &mut DesignApp, ui: &mut egui::Ui, g: &designcraft_color::Gradient, target: &str, mode: ColorMode) {
|
||||
use designcraft_color::Color;
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let sel: RampSel = ui.data(|d| d.get_temp(egui::Id::unique(GRAD_SEL))).unwrap_or(RampSel::Stop(0));
|
||||
let i = match sel {
|
||||
RampSel::Stop(i) | RampSel::Mid(i) => i,
|
||||
}
|
||||
.min(g.stops.len().saturating_sub(1));
|
||||
let Some(stop) = g.stops.get(i) else { return };
|
||||
let c = stop.color;
|
||||
let pct = |v: f32| (v * 100.0).round();
|
||||
let values = match c {
|
||||
Color::Cmyk { c, m, y, k } => format!("C={} M={} Y={} K={}", pct(c), pct(m), pct(y), pct(k)),
|
||||
Color::Gray { k } => format!("K={}", pct(k)),
|
||||
Color::Rgb { .. } => {
|
||||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||||
format!("R={r} G={g} B={b}")
|
||||
}
|
||||
};
|
||||
ui.label(egui::RichText::new(values).size(11.0).color(t.text_dim));
|
||||
let mut new = None;
|
||||
match mode {
|
||||
ColorMode::Cmyk => {
|
||||
let mut v = c.to_cmyk();
|
||||
let mut done = false;
|
||||
for (k, label) in ["C", "M", "Y", "K"].iter().enumerate() {
|
||||
done |= channel(ui, label, &mut v[k], 100.0, "%", |x| cmyk_ramp(k, x));
|
||||
}
|
||||
if done {
|
||||
new = Some(Color::cmyk(v[0], v[1], v[2], v[3]));
|
||||
}
|
||||
}
|
||||
ColorMode::Rgb => {
|
||||
let mut v = c.to_rgb();
|
||||
let mut done = false;
|
||||
for (k, label) in ["R", "G", "B"].iter().enumerate() {
|
||||
done |= channel(ui, label, &mut v[k], 255.0, "", |x| {
|
||||
let mut p = [0.0; 3];
|
||||
p[k] = x;
|
||||
egui::Color32::from_rgb((p[0] * 255.0) as u8, (p[1] * 255.0) as u8, (p[2] * 255.0) as u8)
|
||||
});
|
||||
}
|
||||
if done {
|
||||
new = Some(Color::rgb(v[0], v[1], v[2]));
|
||||
}
|
||||
}
|
||||
ColorMode::Lab => {
|
||||
let lab = c.to_lab();
|
||||
ui.label(egui::RichText::new(format!("L {:.0} a {:.0} b {:.0}", lab.l, lab.a, lab.b)).color(t.text_dim));
|
||||
}
|
||||
}
|
||||
if let Some([r, gr, b]) = spectrum_pick(ui) {
|
||||
new = Some(Color::rgb(r, gr, b));
|
||||
}
|
||||
if let Some(c) = new {
|
||||
let mut stops = stops_json(g);
|
||||
set_stop(&mut stops, i, "color", color_json(&c));
|
||||
let _ = app.run("object.gradient", json!({"stops": stops, "target": target}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The spectrum ramp: hue across, light to dark down; click to apply.
|
||||
fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) {
|
||||
if let Some([r, g, b]) = spectrum_pick(ui) {
|
||||
let text = super::colors_affect_text(app);
|
||||
let _ = app.run("object.color", json!({"color": {"model": "rgb", "r": r, "g": g, "b": b}, "target": target, "text": text}));
|
||||
}
|
||||
}
|
||||
|
||||
/// The spectrum ramp; a click on it gives that colour (RGB, 0–1, in 1/255 steps).
|
||||
fn spectrum_pick(ui: &mut egui::Ui) -> Option<[f32; 3]> {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr(&app.ui.language, "Color spectrum")));
|
||||
let at = |u: f32, v: f32| -> [f32; 3] {
|
||||
@@ -741,14 +818,9 @@ fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) {
|
||||
ui.painter().rect_filled(cell, 0.0, egui::Color32::from_rgb((c[0] * 255.0) as u8, (c[1] * 255.0) as u8, (c[2] * 255.0) as u8));
|
||||
}
|
||||
}
|
||||
if resp.clicked()
|
||||
&& let Some(p) = resp.interact_pointer_pos()
|
||||
{
|
||||
let c = at(((p.x - r.min.x) / r.width()).clamp(0.0, 1.0), ((p.y - r.min.y) / r.height()).clamp(0.0, 1.0));
|
||||
let text = super::colors_affect_text(app);
|
||||
let q = |v: f32| (v * 255.0).round() / 255.0;
|
||||
let _ = app.run("object.color", json!({"color": {"model": "rgb", "r": q(c[0]), "g": q(c[1]), "b": q(c[2])}, "target": target, "text": text}));
|
||||
}
|
||||
let p = resp.interact_pointer_pos().filter(|_| resp.clicked())?;
|
||||
let c = at(((p.x - r.min.x) / r.width()).clamp(0.0, 1.0), ((p.y - r.min.y) / r.height()).clamp(0.0, 1.0));
|
||||
Some(c.map(|v| (v * 255.0).round() / 255.0))
|
||||
}
|
||||
|
||||
/// A swatch dragged out of the Swatches panel (the Gradient panel's ramp takes it as a stop).
|
||||
@@ -898,6 +970,10 @@ fn thumbnail(p: &egui::Painter, r: egui::Rect, g: &designcraft_color::Gradient,
|
||||
p.rect_stroke(r, 0.0, egui::Stroke::new(1.0, border), egui::StrokeKind::Outside);
|
||||
}
|
||||
|
||||
/// Temp-data key of what's selected on the Gradient panel's ramp ([`RampSel`]): the Color panel
|
||||
/// edits that stop.
|
||||
pub(super) const GRAD_SEL: &str = "grad_sel";
|
||||
|
||||
/// The gradient across `ramp`, sampled in bands.
|
||||
fn paint_ramp(painter: &egui::Painter, ramp: egui::Rect, g: &designcraft_color::Gradient) {
|
||||
let n = 96;
|
||||
@@ -956,7 +1032,7 @@ fn gradient_type_combo(ui: &mut egui::Ui, lang: &str, g: &designcraft_color::Gra
|
||||
|
||||
/// What's selected on the ramp: a stop, or the midpoint after a stop.
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum RampSel {
|
||||
pub(super) enum RampSel {
|
||||
Stop(usize),
|
||||
Mid(usize),
|
||||
}
|
||||
@@ -999,7 +1075,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let is_gradient = cur.is_some();
|
||||
let g = cur.unwrap_or_default();
|
||||
let radial = g.kind == designcraft_color::GradientKind::Radial;
|
||||
let sel_id = egui::Id::unique("grad_sel");
|
||||
let sel_id = egui::Id::unique(GRAD_SEL);
|
||||
let drag_id = egui::Id::unique("grad_drag");
|
||||
let mut sel: RampSel = ui.data(|d| d.get_temp(sel_id)).unwrap_or(RampSel::Stop(0));
|
||||
let last = g.stops.len().saturating_sub(1);
|
||||
|
||||
Reference in New Issue
Block a user