|
|
|
@@ -41,13 +41,14 @@
|
|
|
|
|
//! in-progress slider drags); the view observes the engine and re-syncs
|
|
|
|
|
//! the widget values from the engine snapshot on every render.
|
|
|
|
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use crate::oakui::component::text_input;
|
|
|
|
|
|
|
|
|
|
use gpui::effect_stack::EffectId;
|
|
|
|
|
use gpui::colors::DefaultColors;
|
|
|
|
|
use gpui::{
|
|
|
|
|
div, prelude::*, px, rgb, size, point, ClickEvent, Context, Entity, EventEmitter, Render,
|
|
|
|
|
div, img, prelude::*, px, rgb, size, point, ClickEvent, Context, Entity, EventEmitter, Render,
|
|
|
|
|
SharedString, Window,
|
|
|
|
|
};
|
|
|
|
|
use gpui::{
|
|
|
|
@@ -750,7 +751,13 @@ fn wire_controls<E: AppEngine>(view: &OfxParamsView<E>, cx: &mut Context<OfxPara
|
|
|
|
|
color.a as f64,
|
|
|
|
|
]);
|
|
|
|
|
engine.update(cx, |engine, cx| {
|
|
|
|
|
let _ = engine.set_effect_param(effect, &input_id, nv, cx);
|
|
|
|
|
if let Err(err) = engine.set_effect_param(effect, &input_id, nv, cx) {
|
|
|
|
|
// A failed set is the only path that snaps the
|
|
|
|
|
// swatch back to the old engine value (the value
|
|
|
|
|
// sync re-reads it every frame), so log it rather
|
|
|
|
|
// than swallowing it.
|
|
|
|
|
println!("[ofx params] set colour param {input_id:?} failed: {err}");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
@@ -892,6 +899,11 @@ impl<E: AppEngine> Render for OfxParamsView<E> {
|
|
|
|
|
row.into_any_element()
|
|
|
|
|
}
|
|
|
|
|
ControlKind::Color(picker) => {
|
|
|
|
|
// Refresh the "pick from project" strip from the
|
|
|
|
|
// engine's bin every frame (equality inside the
|
|
|
|
|
// picker short-circuits unchanged media).
|
|
|
|
|
let media = collect_project_media(self.engine.read(cx));
|
|
|
|
|
picker.update(cx, |picker, cx| picker.set_project_media(media, cx));
|
|
|
|
|
div().flex_1().child(picker.clone()).into_any_element()
|
|
|
|
|
}
|
|
|
|
|
ControlKind::Text(editor) => {
|
|
|
|
@@ -1004,6 +1016,103 @@ pub enum OfxColorEvent {
|
|
|
|
|
Committed(Rgba),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A project media entry offered by the colour picker's "pick from
|
|
|
|
|
/// project" strip (a flattened view of the bin's entries).
|
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
|
pub struct ProjectMedia {
|
|
|
|
|
/// The entry's stable id (host-side key).
|
|
|
|
|
id: u64,
|
|
|
|
|
/// The display name.
|
|
|
|
|
name: String,
|
|
|
|
|
/// An asset path for the thumbnail, if any.
|
|
|
|
|
thumbnail: Option<PathBuf>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<gpui_widgets::project_explorer::ProjectEntry> for ProjectMedia {
|
|
|
|
|
fn from(entry: gpui_widgets::project_explorer::ProjectEntry) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: entry.id,
|
|
|
|
|
name: entry.name.to_string(),
|
|
|
|
|
thumbnail: entry.thumbnail.map(|p| PathBuf::from(p.to_string())),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Flatten the engine's project entries into the picker's media strip: a
|
|
|
|
|
/// directory root contributes its direct file children; a file root is
|
|
|
|
|
/// itself an entry. Matches the project explorer's icon view.
|
|
|
|
|
fn collect_project_media<E: AppEngine>(engine: &E) -> Vec<ProjectMedia> {
|
|
|
|
|
let mut media = Vec::new();
|
|
|
|
|
for root in engine.roots() {
|
|
|
|
|
if root.is_dir {
|
|
|
|
|
for child in engine.children(root.id) {
|
|
|
|
|
if !child.is_dir {
|
|
|
|
|
media.push(ProjectMedia::from(child));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
media.push(ProjectMedia::from(root));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
media
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sample an [`image::RgbaImage`] at unit coordinates (0..1 each);
|
|
|
|
|
/// out-of-range coordinates clamp to the nearest edge pixel.
|
|
|
|
|
fn sample_rgba8(img: &image::RgbaImage, u: f32, v: f32) -> Option<Rgba> {
|
|
|
|
|
let w = img.width();
|
|
|
|
|
let h = img.height();
|
|
|
|
|
if w == 0 || h == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let x = (u.clamp(0.0, 1.0) * (w - 1) as f32).round() as u32;
|
|
|
|
|
let y = (v.clamp(0.0, 1.0) * (h - 1) as f32).round() as u32;
|
|
|
|
|
let p = img.get_pixel(x, y);
|
|
|
|
|
Some(Rgba {
|
|
|
|
|
r: p[0] as f32 / 255.0,
|
|
|
|
|
g: p[1] as f32 / 255.0,
|
|
|
|
|
b: p[2] as f32 / 255.0,
|
|
|
|
|
a: p[3] as f32 / 255.0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Map a point inside a `box_w` x `box_h` widget to unit coordinates in an
|
|
|
|
|
/// `img_w` x `img_h` image laid out with `ObjectFit::Contain` (the `img`
|
|
|
|
|
/// default). Returns `None` when the point falls in the letterbox.
|
|
|
|
|
fn contain_uv(
|
|
|
|
|
img_w: u32,
|
|
|
|
|
img_h: u32,
|
|
|
|
|
box_w: f32,
|
|
|
|
|
box_h: f32,
|
|
|
|
|
u: f32,
|
|
|
|
|
v: f32,
|
|
|
|
|
) -> Option<(f32, f32)> {
|
|
|
|
|
let (w, h) = (img_w as f32, img_h as f32);
|
|
|
|
|
if w <= 0.0 || h <= 0.0 || box_w <= 0.0 || box_h <= 0.0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let scale = (box_w / w).min(box_h / h);
|
|
|
|
|
let cw = w * scale;
|
|
|
|
|
let ch = h * scale;
|
|
|
|
|
let ox = (box_w - cw) / 2.0;
|
|
|
|
|
let oy = (box_h - ch) / 2.0;
|
|
|
|
|
let px = u * box_w - ox;
|
|
|
|
|
let py = v * box_h - oy;
|
|
|
|
|
if px < 0.0 || py < 0.0 || px > cw || py > ch {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
Some((px / cw, py / ch))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load `path` and sample it at unit coordinates, assuming the image is
|
|
|
|
|
/// laid out with `ObjectFit::Contain` in a `box_w` x `box_h` box. `None`
|
|
|
|
|
/// when the file can't be decoded or the point lands in the letterbox.
|
|
|
|
|
fn sample_pixel(path: &Path, box_w: f32, box_h: f32, u: f32, v: f32) -> Option<Rgba> {
|
|
|
|
|
let img = image::open(path).ok()?.to_rgba8();
|
|
|
|
|
let (uu, vv) = contain_uv(img.width(), img.height(), box_w, box_h, u, v)?;
|
|
|
|
|
sample_rgba8(&img, uu, vv)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A colour swatch with a deferred popup picker, used for OFX colour
|
|
|
|
|
/// parameters (the replacement for the old per-channel spinboxes).
|
|
|
|
|
///
|
|
|
|
@@ -1044,6 +1153,8 @@ pub struct OfxColorPicker {
|
|
|
|
|
a: Entity<Slider>,
|
|
|
|
|
/// The hex editor (`#RRGGBB` / `#RRGGBBAA`).
|
|
|
|
|
hex: Entity<EditableTextState>,
|
|
|
|
|
/// The "pick from project" media strip (thumbnails to sample from).
|
|
|
|
|
media: Vec<ProjectMedia>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl OfxColorPicker {
|
|
|
|
@@ -1067,6 +1178,7 @@ impl OfxColorPicker {
|
|
|
|
|
b: Self::channel_slider(cx, window, 2, color.b as f64),
|
|
|
|
|
a: Self::channel_slider(cx, window, 3, color.a as f64),
|
|
|
|
|
hex: cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)),
|
|
|
|
|
media: Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
let hex_text = format_hex(color);
|
|
|
|
|
picker.hex.update(cx, |hex, cx| hex.emplace(&hex_text, cx));
|
|
|
|
@@ -1157,6 +1269,17 @@ impl OfxColorPicker {
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Refresh the "pick from project" media strip. No-op when unchanged
|
|
|
|
|
/// (the params view pushes this every frame; equality short-circuits
|
|
|
|
|
/// the notify so the strip does not re-render endlessly).
|
|
|
|
|
pub(crate) fn set_project_media(&mut self, media: Vec<ProjectMedia>, cx: &mut Context<Self>) {
|
|
|
|
|
if self.media == media {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.media = media;
|
|
|
|
|
cx.notify();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_menu(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) {
|
|
|
|
|
if !self.open {
|
|
|
|
|
self.open = true;
|
|
|
|
@@ -1175,6 +1298,9 @@ impl OfxColorPicker {
|
|
|
|
|
if self.open {
|
|
|
|
|
self.open = false;
|
|
|
|
|
self.hex_error = false;
|
|
|
|
|
// Discard any in-progress draft edits so the sliders / swatch
|
|
|
|
|
// settle back on the committed colour.
|
|
|
|
|
self.draft = self.committed;
|
|
|
|
|
self.sync_from_draft(cx);
|
|
|
|
|
cx.emit(OfxColorEvent::Cancelled);
|
|
|
|
|
cx.notify();
|
|
|
|
@@ -1318,6 +1444,85 @@ impl OfxColorPicker {
|
|
|
|
|
}));
|
|
|
|
|
let buttons = div().flex().justify_between().gap_1().child(cancel).child(ok);
|
|
|
|
|
|
|
|
|
|
// "Pick from project": a strip of bin thumbnails; clicking one
|
|
|
|
|
// samples the pixel under the cursor into the draft. Thumbnails are
|
|
|
|
|
// `img` (ObjectFit::Contain), and the strip's canvas captures the
|
|
|
|
|
// rendered bounds so the click can be mapped back into the image.
|
|
|
|
|
let media_section = if self.media.is_empty() {
|
|
|
|
|
div().into_any_element()
|
|
|
|
|
} else {
|
|
|
|
|
let mut strip = div().flex().flex_wrap().gap_1();
|
|
|
|
|
for media in &self.media {
|
|
|
|
|
let Some(thumb) = &media.thumbnail else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let id = media.id;
|
|
|
|
|
let thumb = thumb.clone();
|
|
|
|
|
let bounds_cell = Arc::new(Mutex::new(None::<Bounds<Pixels>>));
|
|
|
|
|
let bounds_cell_prepaint = bounds_cell.clone();
|
|
|
|
|
let thumb_click = thumb.clone();
|
|
|
|
|
strip = strip.child(
|
|
|
|
|
div()
|
|
|
|
|
.id(ElementId::named_usize("ofx-color-project-thumb", id as usize))
|
|
|
|
|
.debug_selector(move || format!("ofx-color-project-thumb-{id}").into())
|
|
|
|
|
.relative()
|
|
|
|
|
.w(px(72.0))
|
|
|
|
|
.h(px(48.0))
|
|
|
|
|
.overflow_hidden()
|
|
|
|
|
.rounded_sm()
|
|
|
|
|
.border_1()
|
|
|
|
|
.border_color(colors.border)
|
|
|
|
|
.cursor_pointer()
|
|
|
|
|
.child(img(thumb).w(px(72.0)).h(px(48.0)))
|
|
|
|
|
.child(
|
|
|
|
|
canvas(
|
|
|
|
|
move |bounds, _window, _cx| {
|
|
|
|
|
*bounds_cell_prepaint.lock().unwrap() = Some(bounds);
|
|
|
|
|
},
|
|
|
|
|
|_bounds, _content, _window, _cx| {},
|
|
|
|
|
)
|
|
|
|
|
.absolute()
|
|
|
|
|
.inset_0(),
|
|
|
|
|
)
|
|
|
|
|
.on_mouse_down(
|
|
|
|
|
MouseButton::Left,
|
|
|
|
|
cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
|
|
|
|
|
let Some(bounds) = bounds_cell.lock().unwrap().take() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let pos = event.position - bounds.origin;
|
|
|
|
|
let u = f32::from(pos.x) / f32::from(bounds.size.width);
|
|
|
|
|
let v = f32::from(pos.y) / f32::from(bounds.size.height);
|
|
|
|
|
let Some(color) = sample_pixel(&thumb_click, 72.0, 48.0, u, v)
|
|
|
|
|
else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
this.draft = color;
|
|
|
|
|
this.hex_error = false;
|
|
|
|
|
this.sync_from_draft(cx);
|
|
|
|
|
cx.notify();
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
div()
|
|
|
|
|
.flex()
|
|
|
|
|
.flex_col()
|
|
|
|
|
.gap_1()
|
|
|
|
|
.pt_1()
|
|
|
|
|
.mt_1()
|
|
|
|
|
.border_t_1()
|
|
|
|
|
.border_color(colors.border)
|
|
|
|
|
.child(
|
|
|
|
|
div()
|
|
|
|
|
.text_xs()
|
|
|
|
|
.text_color(colors.text)
|
|
|
|
|
.child(crate::i18n::tr("ofx.color.pick_project")),
|
|
|
|
|
)
|
|
|
|
|
.child(strip)
|
|
|
|
|
.into_any_element()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
deferred(
|
|
|
|
|
anchored()
|
|
|
|
|
.position(self.position)
|
|
|
|
@@ -1350,7 +1555,8 @@ impl OfxColorPicker {
|
|
|
|
|
.child(slider_rows)
|
|
|
|
|
.child(hex_row)
|
|
|
|
|
.child(error_hint)
|
|
|
|
|
.child(buttons),
|
|
|
|
|
.child(buttons)
|
|
|
|
|
.child(media_section),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.with_priority(1)
|
|
|
|
@@ -1363,7 +1569,9 @@ impl Render for OfxColorPicker {
|
|
|
|
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
|
|
|
let colors = cx.default_colors().clone();
|
|
|
|
|
let control = self.control;
|
|
|
|
|
let committed = self.committed;
|
|
|
|
|
// While the popup is open the swatch follows the draft live (the
|
|
|
|
|
// user's in-progress edit), otherwise it shows the committed value.
|
|
|
|
|
let swatch_color = if self.open { self.draft } else { self.committed };
|
|
|
|
|
|
|
|
|
|
let swatch = div()
|
|
|
|
|
.id(ElementId::named_usize("ofx-color-swatch", control))
|
|
|
|
@@ -1396,7 +1604,7 @@ impl Render for OfxColorPicker {
|
|
|
|
|
.child(canvas(
|
|
|
|
|
|bounds, _window, _cx| bounds,
|
|
|
|
|
move |bounds, _content, window, cx| {
|
|
|
|
|
paint_checker_swatch(bounds, committed, window, cx);
|
|
|
|
|
paint_checker_swatch(bounds, swatch_color, window, cx);
|
|
|
|
|
},
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
@@ -1481,7 +1689,7 @@ fn paint_checker_swatch(
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use gpui::TestAppContext;
|
|
|
|
|
use gpui::{Modifiers, TestAppContext};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn hex_parsing_and_formatting() {
|
|
|
|
@@ -1579,4 +1787,170 @@ mod tests {
|
|
|
|
|
"the colour swatch should be painted"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn sample_rgba8_reads_corners() {
|
|
|
|
|
let mut img = image::RgbaImage::new(4, 2);
|
|
|
|
|
img.put_pixel(0, 0, image::Rgba([255, 0, 0, 255]));
|
|
|
|
|
img.put_pixel(3, 0, image::Rgba([0, 255, 0, 255]));
|
|
|
|
|
img.put_pixel(0, 1, image::Rgba([0, 0, 255, 255]));
|
|
|
|
|
img.put_pixel(3, 1, image::Rgba([255, 255, 255, 128]));
|
|
|
|
|
|
|
|
|
|
let close = |a: Rgba, b: Rgba| {
|
|
|
|
|
(a.r - b.r).abs() < 1e-6
|
|
|
|
|
&& (a.g - b.g).abs() < 1e-6
|
|
|
|
|
&& (a.b - b.b).abs() < 1e-6
|
|
|
|
|
&& (a.a - b.a).abs() < 1e-6
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let red = Rgba { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
|
|
|
|
let green = Rgba { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
|
|
|
|
|
let blue = Rgba { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
|
|
|
|
|
let white_half = Rgba { r: 1.0, g: 1.0, b: 1.0, a: 128.0 / 255.0 };
|
|
|
|
|
assert!(close(sample_rgba8(&img, 0.0, 0.0).unwrap(), red), "top-left");
|
|
|
|
|
assert!(close(sample_rgba8(&img, 1.0, 0.0).unwrap(), green), "top-right");
|
|
|
|
|
assert!(close(sample_rgba8(&img, 0.0, 1.0).unwrap(), blue), "bottom-left");
|
|
|
|
|
assert!(
|
|
|
|
|
close(sample_rgba8(&img, 1.0, 1.0).unwrap(), white_half),
|
|
|
|
|
"bottom-right translucent"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Out-of-range coordinates clamp to the nearest edge pixel.
|
|
|
|
|
assert!(close(sample_rgba8(&img, 2.0, -0.5).unwrap(), green), "clamps u/v");
|
|
|
|
|
assert!(close(sample_rgba8(&img, -1.0, 2.0).unwrap(), blue), "clamps negative");
|
|
|
|
|
// (0.5, 0.5) rounds to pixel (2, 1), which was never set (transparent
|
|
|
|
|
// black) — exercises the rounding path.
|
|
|
|
|
let mid = sample_rgba8(&img, 0.5, 0.5).unwrap();
|
|
|
|
|
assert!(mid.r.abs() < 1e-6 && mid.a.abs() < 1e-6, "middle pixel unset");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn contain_uv_maps_letterbox() {
|
|
|
|
|
// A 4x3 image inside a 72x48 box: scale = min(18, 16) = 16, so the
|
|
|
|
|
// content is 64x48, letterboxed by 4px on each side.
|
|
|
|
|
assert_eq!(contain_uv(4, 3, 72.0, 48.0, 0.5, 0.5), Some((0.5, 0.5)));
|
|
|
|
|
assert_eq!(contain_uv(4, 3, 72.0, 48.0, 0.0, 0.5), None, "left letterbox");
|
|
|
|
|
assert_eq!(contain_uv(4, 3, 72.0, 48.0, 1.0, 0.5), None, "right letterbox");
|
|
|
|
|
// The content's right edge sits at u = 4/72 + 64/72.
|
|
|
|
|
let right = contain_uv(4, 3, 72.0, 48.0, 68.0 / 72.0, 0.5).unwrap();
|
|
|
|
|
assert!((right.0 - 1.0).abs() < 1e-6 && (right.1 - 0.5).abs() < 1e-6);
|
|
|
|
|
// Degenerate boxes / images sample nothing.
|
|
|
|
|
assert_eq!(contain_uv(0, 3, 72.0, 48.0, 0.5, 0.5), None);
|
|
|
|
|
assert_eq!(contain_uv(4, 3, 0.0, 48.0, 0.5, 0.5), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn sample_pixel_reads_png_file() {
|
|
|
|
|
let dir = std::env::temp_dir().join(format!("oak-ofx-sample-{}", std::process::id()));
|
|
|
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
|
|
|
let path = dir.join("flat.png");
|
|
|
|
|
let img = image::RgbaImage::from_pixel(2, 2, image::Rgba([0x1A, 0x80, 0xE6, 0xFF]));
|
|
|
|
|
img.save(&path).unwrap();
|
|
|
|
|
|
|
|
|
|
// 2x2 in a 72x48 box: scale = 24 -> 48x48 content, 12px side bars.
|
|
|
|
|
let c = sample_pixel(&path, 72.0, 48.0, 0.5, 0.5).expect("centre samples");
|
|
|
|
|
assert!((c.r - 0x1A as f32 / 255.0).abs() < 1e-6);
|
|
|
|
|
assert!((c.g - 0x80 as f32 / 255.0).abs() < 1e-6);
|
|
|
|
|
assert!((c.b - 0xE6 as f32 / 255.0).abs() < 1e-6);
|
|
|
|
|
assert!((c.a - 1.0).abs() < 1e-6);
|
|
|
|
|
// A click in the left letterbox has no image pixel under it.
|
|
|
|
|
assert!(sample_pixel(&path, 72.0, 48.0, 0.0, 0.5).is_none());
|
|
|
|
|
// A missing file samples nothing.
|
|
|
|
|
assert!(sample_pixel(&dir.join("nope.png"), 72.0, 48.0, 0.5, 0.5).is_none());
|
|
|
|
|
|
|
|
|
|
std::fs::remove_file(&path).unwrap();
|
|
|
|
|
std::fs::remove_dir(&dir).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Opening the popup and clicking a project thumbnail samples the pixel
|
|
|
|
|
/// under the cursor into the draft (the "pick from project" strip).
|
|
|
|
|
#[gpui::test]
|
|
|
|
|
async fn project_eyedropper_samples_into_draft(cx: &mut TestAppContext) {
|
|
|
|
|
struct Host {
|
|
|
|
|
picker: Entity<OfxColorPicker>,
|
|
|
|
|
}
|
|
|
|
|
impl Render for Host {
|
|
|
|
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
|
|
|
|
div().size_full().child(self.picker.clone())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4x4 source image: red centre pixel, black elsewhere. In a 72x48
|
|
|
|
|
// thumbnail (scale 12 -> 48x48 content) the strip's centre maps to
|
|
|
|
|
// image pixel (2, 2).
|
|
|
|
|
let dir = std::env::temp_dir().join(format!("oak-ofx-eyedrop-{}", std::process::id()));
|
|
|
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
|
|
|
let path = dir.join("clip.png");
|
|
|
|
|
let mut img = image::RgbaImage::new(4, 4);
|
|
|
|
|
img.put_pixel(2, 2, image::Rgba([255, 0, 0, 255]));
|
|
|
|
|
img.save(&path).unwrap();
|
|
|
|
|
|
|
|
|
|
cx.update(|cx| cx.init_colors());
|
|
|
|
|
let window = cx.open_window(size(px(320.0), px(400.0)), |window, cx| {
|
|
|
|
|
let picker = cx.new(|cx| {
|
|
|
|
|
OfxColorPicker::new(
|
|
|
|
|
1,
|
|
|
|
|
Rgba {
|
|
|
|
|
r: 0.0,
|
|
|
|
|
g: 0.0,
|
|
|
|
|
b: 0.0,
|
|
|
|
|
a: 1.0,
|
|
|
|
|
},
|
|
|
|
|
window,
|
|
|
|
|
cx,
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
Host { picker }
|
|
|
|
|
});
|
|
|
|
|
cx.run_until_parked();
|
|
|
|
|
|
|
|
|
|
let mut visual = gpui::VisualTestContext::from_window(window.into(), cx).into_mut();
|
|
|
|
|
visual.update(|window, cx| {
|
|
|
|
|
window.draw(cx).clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Open the popup by clicking the swatch.
|
|
|
|
|
let swatch = visual.debug_bounds("ofx-color-swatch").expect("swatch painted");
|
|
|
|
|
let swatch_center = Point::new(
|
|
|
|
|
swatch.origin.x + swatch.size.width * 0.5,
|
|
|
|
|
swatch.origin.y + swatch.size.height * 0.5,
|
|
|
|
|
);
|
|
|
|
|
visual.simulate_click(swatch_center, Modifiers::default());
|
|
|
|
|
|
|
|
|
|
// The params view normally pushes the media strip every frame; here
|
|
|
|
|
// the Host injects it directly, then re-draws so the strip lays out.
|
|
|
|
|
let host = window.root(cx).expect("host root");
|
|
|
|
|
visual.update(|window, cx| {
|
|
|
|
|
host.update(cx, |host, cx| {
|
|
|
|
|
host.picker.update(cx, |picker, cx| {
|
|
|
|
|
picker.set_project_media(
|
|
|
|
|
vec![ProjectMedia {
|
|
|
|
|
id: 7,
|
|
|
|
|
name: "clip.png".into(),
|
|
|
|
|
thumbnail: Some(path.clone()),
|
|
|
|
|
}],
|
|
|
|
|
cx,
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
window.draw(cx).clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Click the thumbnail's centre: samples the red pixel into the draft.
|
|
|
|
|
let thumb = visual
|
|
|
|
|
.debug_bounds("ofx-color-project-thumb-7")
|
|
|
|
|
.expect("thumbnail painted");
|
|
|
|
|
let thumb_center = Point::new(
|
|
|
|
|
thumb.origin.x + thumb.size.width * 0.5,
|
|
|
|
|
thumb.origin.y + thumb.size.height * 0.5,
|
|
|
|
|
);
|
|
|
|
|
visual.simulate_click(thumb_center, Modifiers::default());
|
|
|
|
|
|
|
|
|
|
let draft = cx.read(|cx| host.read(cx).picker.read(cx).draft);
|
|
|
|
|
assert!((draft.r - 1.0).abs() < 1e-6, "draft should sample red, got {draft:?}");
|
|
|
|
|
assert!(draft.g.abs() < 1e-6 && draft.b.abs() < 1e-6, "no green/blue leaked");
|
|
|
|
|
|
|
|
|
|
std::fs::remove_file(&path).unwrap();
|
|
|
|
|
std::fs::remove_dir(&dir).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|