M3.23: per-item Opacity popups and one grouped blend-mode widget
Clicking an Opacity row in the Appearance panel (a fill's or stroke's, or the object's) opens a popup with the same blend-mode dropdown, opacity field and slider as the Transparency panel (widgets::opacity_blend), and edits that item through transparency.set. The blend-mode dropdown draws a separator between the BlendMode::group() groups, also for the blend-mode parameters of effect dialogs and inline effect editors. Fills and strokes of type objects composite with their own opacity and blend mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
543d09985d
commit
b6b87ee9b6
|
|
@ -231,6 +231,12 @@ impl AppearanceItem {
|
|||
pub fn kind_name(&self) -> &'static str {
|
||||
if self.is_fill() { "fill" } else { "stroke" }
|
||||
}
|
||||
pub fn paint(&self) -> &Paint {
|
||||
match self {
|
||||
AppearanceItem::Fill(f) => &f.paint,
|
||||
AppearanceItem::Stroke(s) => &s.paint,
|
||||
}
|
||||
}
|
||||
pub fn visible(&self) -> bool {
|
||||
match self {
|
||||
AppearanceItem::Fill(f) => f.visible,
|
||||
|
|
|
|||
|
|
@ -245,3 +245,25 @@ fn per_fill_offset_path_renders() {
|
|||
let px = render(&s).pixel(90, 150);
|
||||
assert!(px[0] > 200 && px[3] > 200, "{px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_fill_items_composite_with_their_opacity() {
|
||||
let mut s = session();
|
||||
let t = NodeId(run(&mut s, "text.create", json!({"x": 20, "y": 200, "text": "HIM", "size": 150}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "select.set", json!({"ids": [t.0]}));
|
||||
run(&mut s, "appearance.addFill", json!({}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "color": "#ff0000"}));
|
||||
let render = |s: &Session| {
|
||||
vectorcraft_render::Renderer::new().render(&s.doc().unwrap().doc, 400, 400, vectorcraft_geom::Affine::IDENTITY, &Default::default())
|
||||
};
|
||||
let opaque = render(&s);
|
||||
// A pixel the red fill covers fully (over the black character fill).
|
||||
let (x, y) = (0..400u32).flat_map(|y| (0..400u32).map(move |x| (x, y))).find(|&(x, y)| opaque.pixel(x, y) == [255, 0, 0, 255]).expect("red text");
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "opacity": 0.5, "blend": "multiply"}));
|
||||
let px = render(&s).pixel(x, y);
|
||||
// Half of red multiplied over black: black.
|
||||
assert!(px[0] < 20 && px[3] == 255, "{px:?}");
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "blend": "normal"}));
|
||||
let px = render(&s).pixel(x, y);
|
||||
assert!((110..=145).contains(&px[0]) && px[1] < 10, "{px:?}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -798,19 +798,28 @@ impl Renderer {
|
|||
if !n.appearance.items.is_empty() {
|
||||
let mut all = g.all.clone();
|
||||
all.apply_affine(t.xf);
|
||||
for item in &n.appearance.items {
|
||||
for item in n.appearance.items.iter().filter(|i| i.visible() && !i.paint().is_none()) {
|
||||
// Each item composites with its own opacity and blend mode.
|
||||
let layered = item.opacity() < 1.0 || item.blend() != vectorcraft_color::BlendMode::Normal;
|
||||
if layered {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(blend_mode(item.blend())), Some(item.opacity()), None, None);
|
||||
}
|
||||
ctx.set_transform(f.view);
|
||||
match item {
|
||||
AppearanceItem::Fill(fl) if fl.visible && paint::set_paint(ctx, &fl.paint, tb, f.doc) => {
|
||||
AppearanceItem::Fill(fl) if paint::set_paint(ctx, &fl.paint, tb, f.doc) => {
|
||||
ctx.set_fill_rule(peniko::Fill::NonZero);
|
||||
ctx.fill_path(&all);
|
||||
}
|
||||
AppearanceItem::Stroke(st) if st.visible && st.width > 0.0 && paint::set_paint(ctx, &st.paint, tb, f.doc) => {
|
||||
AppearanceItem::Stroke(st) if st.width > 0.0 && paint::set_paint(ctx, &st.paint, tb, f.doc) => {
|
||||
ctx.set_stroke(kurbo::Stroke::new(st.width));
|
||||
ctx.stroke_path(&all);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if layered {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,14 @@ pub(super) fn param_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|||
d.fields.iter().filter(|(k, _)| !k.starts_with("__") && k.as_str() != "preview").map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
for (k, v) in keys {
|
||||
ui.label(egui::RichText::new(humanize(&k)).color(t.text));
|
||||
if let Some(cur) = crate::widgets::blend_param(&k, &v) {
|
||||
if let Some(m) = crate::widgets::blend_param_dropdown(ui, ("fx-blend", &k), cur) {
|
||||
d.fields.insert(k, m);
|
||||
changed = true;
|
||||
}
|
||||
ui.end_row();
|
||||
continue;
|
||||
}
|
||||
match v {
|
||||
Value::Number(n) => {
|
||||
let mut x = n.as_f64().unwrap_or(0.0);
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ use serde_json::{Value, json};
|
|||
use vectorcraft_color::{BlendMode, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Effect, Node, NodeId};
|
||||
|
||||
use super::{first_selected, pstate, set_pstate};
|
||||
use super::{first_selected, live_run, pstate, set_pstate};
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
use crate::widgets::{self, TransparencyEdit, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
const ROW: f32 = 30.0;
|
||||
|
|
@ -221,11 +221,7 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel) {
|
|||
let open: bool = pstate(ui.ctx(), &format!("ap-open-{i}"));
|
||||
let (r, resp) = row(ui, sel == Sel::Item(i));
|
||||
row_rects.push((i, r));
|
||||
let visible = it.visible();
|
||||
let (paint, width) = match it {
|
||||
AppearanceItem::Fill(f) => (&f.paint, 0.0),
|
||||
AppearanceItem::Stroke(s) => (&s.paint, s.width),
|
||||
};
|
||||
let (visible, paint) = (it.visible(), it.paint());
|
||||
let is_stroke = !it.is_fill();
|
||||
if eye(ui, r, ("ap-eye", i), visible, true) {
|
||||
app.run("appearance.setItem", json!({"index": i, "visible": !visible})).ok();
|
||||
|
|
@ -249,10 +245,10 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel) {
|
|||
egui::Popup::menu(&cresp).show(|ui| {
|
||||
swatch_picker(app, ui, i);
|
||||
});
|
||||
if is_stroke {
|
||||
if let AppearanceItem::Stroke(st) = it {
|
||||
let fr = Rect::from_min_size(pos2(r.left() + EYE_W + 104.0, r.center().y - 12.0), vec2(56.0, 24.0));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(fr).layout(egui::Layout::left_to_right(egui::Align::Center)));
|
||||
if let Some(w) = widgets::num_field(&mut child, ("ap-w", i), Some(width), vectorcraft_doc::Unit::Points, 56.0) {
|
||||
if let Some(w) = widgets::num_field(&mut child, ("ap-w", i), Some(st.width), vectorcraft_doc::Unit::Points, 56.0) {
|
||||
app.run("appearance.setItem", json!({"index": i, "weight": w})).ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -269,7 +265,7 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel) {
|
|||
stopped = true;
|
||||
}
|
||||
if open {
|
||||
sub_row(app, ui, ("ap-sub-op", i), &format!("Opacity: {}", opacity_text(it.opacity(), it.blend())), true, 1);
|
||||
opacity_row(app, ui, Some(i), it.opacity(), it.blend());
|
||||
for (k, e) in it.effects().iter().enumerate() {
|
||||
effect_row(app, ui, Some(i), k, e, sel == Sel::ItemEffect(i, k));
|
||||
}
|
||||
|
|
@ -297,14 +293,7 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel) {
|
|||
for (k, e) in n.appearance.effects.iter().enumerate() {
|
||||
effect_row(app, ui, None, k, e, sel == Sel::Effect(k));
|
||||
}
|
||||
// Object opacity row.
|
||||
let (r, resp) = row(ui, false);
|
||||
eye(ui, r, "ap-op-eye", true, false);
|
||||
let lx = r.left() + EYE_W + 24.0;
|
||||
if link(ui, pos2(lx, r.center().y), "ap-op-link", "Opacity:") || resp.double_clicked() {
|
||||
app.ui.open_panel = Some("transparency".into());
|
||||
}
|
||||
text(ui, pos2(lx + 56.0, r.center().y), &opacity_text(n.opacity, n.blend), false);
|
||||
opacity_row(app, ui, None, n.opacity, n.blend);
|
||||
if let Some((from, to_row)) = drop
|
||||
&& from != to_row
|
||||
{
|
||||
|
|
@ -320,10 +309,24 @@ fn target_index(rows: &[(usize, Rect)], y: f32) -> usize {
|
|||
})
|
||||
}
|
||||
|
||||
fn sub_row(_app: &mut VectorcraftApp, ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug + Copy, label: &str, visible: bool, depth: usize) {
|
||||
let (r, _) = row(ui, false);
|
||||
eye(ui, r, (id, "eye"), visible, false);
|
||||
text(ui, r.left_center() + vec2(EYE_W + 24.0 + depth as f32 * 12.0, 0.0), label, false);
|
||||
/// The Opacity row of the object (`item: None`) or, indented, of fill/stroke `item`. Clicking it
|
||||
/// opens a popup with the shared opacity and blend controls (`transparency.set` on that item).
|
||||
fn opacity_row(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, opacity: f32, blend: BlendMode) {
|
||||
let (r, resp) = row(ui, false);
|
||||
eye(ui, r, ("ap-op-eye", item), true, false);
|
||||
let lx = r.left() + EYE_W + 24.0 + if item.is_some() { 12.0 } else { 0.0 };
|
||||
let clicked = link(ui, pos2(lx, r.center().y), ("ap-op-link", item), "Opacity:") || resp.clicked();
|
||||
text(ui, pos2(lx + 56.0, r.center().y), &opacity_text(opacity, blend), false);
|
||||
egui::Popup::menu(&resp)
|
||||
.open_memory(clicked.then_some(egui::SetOpenCommand::Toggle))
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.show(|ui| match widgets::opacity_blend(ui, ("ap-op", item), opacity, blend, true) {
|
||||
Some(TransparencyEdit::Blend(b)) => {
|
||||
app.run("transparency.set", json!({"item": item, "blend": b.label()})).ok();
|
||||
}
|
||||
Some(TransparencyEdit::Opacity(o, phase)) => live_run(app, "Opacity", "transparency.set", json!({"item": item, "opacity": o}), phase),
|
||||
None => {}
|
||||
});
|
||||
}
|
||||
|
||||
/// An effect row: of the object (`item: None`) or, indented under it, of fill/stroke `item`.
|
||||
|
|
@ -379,6 +382,12 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, k:
|
|||
ui.horizontal(|ui| {
|
||||
let label = humanize(key);
|
||||
ui.add_sized(vec2(84.0, 22.0), egui::Label::new(egui::RichText::new(label).size(11.5).color(t.text)).truncate());
|
||||
if let Some(cur) = widgets::blend_param(key, v) {
|
||||
if let Some(m) = widgets::blend_param_dropdown(ui, ("fx-blend", item, k, key.as_str()), cur) {
|
||||
change = Some((key.clone(), m));
|
||||
}
|
||||
return;
|
||||
}
|
||||
match v {
|
||||
Value::Bool(b) => {
|
||||
if widgets::check(ui, "", *b, true) {
|
||||
|
|
|
|||
|
|
@ -126,3 +126,34 @@ fn item_effect_rows_toggle_their_eye() {
|
|||
fn current_stroke_effects(app: &VectorcraftApp) -> Vec<vectorcraft_doc::Effect> {
|
||||
first_selected(app).unwrap().appearance.items[1].effects().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_separators_follow_the_groups() {
|
||||
use vectorcraft_color::BlendMode;
|
||||
let starts: Vec<BlendMode> =
|
||||
(0..BlendMode::ALL.len()).filter(|i| crate::widgets::blend_separator_before(*i)).map(|i| BlendMode::ALL[i]).collect();
|
||||
assert_eq!(starts, [BlendMode::Darken, BlendMode::Lighten, BlendMode::Overlay, BlendMode::Difference, BlendMode::Hue]);
|
||||
assert!(starts.iter().all(|m| m.group() > 0) && !crate::widgets::blend_separator_before(BlendMode::ALL.len()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_opacity_popup_sets_the_items_opacity() {
|
||||
let mut app = app_with_rect();
|
||||
let ctx = egui::Context::default();
|
||||
super::set_pstate(&ctx, "ap-open-1", true);
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
// The stroke's Opacity sub-row (listed first; the object's own Opacity row is last).
|
||||
let link = texts.iter().find(|(t, _)| t == "Opacity:").expect("item opacity row").1;
|
||||
click(&ctx, &mut app, link.center(), appearance::show);
|
||||
let texts = frame_events(&ctx, &mut app, vec![], appearance::show);
|
||||
assert!(texts.iter().any(|(t, _)| t == "Normal"), "the popup shows the blend mode: {texts:?}");
|
||||
// Type 50 into the popup's opacity field.
|
||||
let field = text_rect(&texts, "100%");
|
||||
click(&ctx, &mut app, field.center(), appearance::show);
|
||||
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
frame_events(&ctx, &mut app, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("50".into())], appearance::show);
|
||||
frame_events(&ctx, &mut app, vec![key(egui::Key::Enter, Default::default())], appearance::show);
|
||||
let n = first_selected(&app).unwrap();
|
||||
assert_eq!(n.appearance.items[1].opacity(), 0.5);
|
||||
assert_eq!((n.appearance.items[0].opacity(), n.opacity), (1.0, 1.0));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use vectorcraft_color::{BlendMode, Paint};
|
|||
|
||||
use super::{current_paints, current_transparency, first_selected, live_run, pstate, selection_len, set_pstate};
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, Live, menu_item};
|
||||
use crate::widgets::{self, TransparencyEdit, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
|
@ -27,36 +27,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
Some(_) => n.as_ref().map(|n| (n.opacity, n.blend)),
|
||||
}
|
||||
.unwrap_or((1.0, BlendMode::Normal));
|
||||
ui.horizontal(|ui| {
|
||||
let labels: Vec<&str> = BlendMode::ALL.iter().map(|b| b.label()).collect();
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
if let Some(i) = widgets::dropdown(ui, "tr-blend", blend.label(), &labels, 104.0) {
|
||||
app.run("transparency.set", json!({"blend": labels[i]})).ok();
|
||||
}
|
||||
});
|
||||
widgets::dim_label(ui, "Opacity:");
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
if let Some(o) = widgets::plain_field(ui, "tr-op", op as f64 * 100.0, "%", 0, 50.0) {
|
||||
app.run("transparency.set", json!({"opacity": o.clamp(0.0, 100.0)})).ok();
|
||||
}
|
||||
});
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(18.0, 26.0), if has { Sense::click() } else { Sense::hover() });
|
||||
ui.painter().rect_stroke(r, 2, Stroke::new(1.0, t.input_border), StrokeKind::Inside);
|
||||
icons::paint(ui, "chevron-right", r.shrink2(vec2(3.0, 7.0)), if has { t.icon } else { t.text_disabled });
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
let mut o = op * 100.0;
|
||||
let r = ui.add(egui::Slider::new(&mut o, 0.0..=100.0).show_value(false));
|
||||
let phase = if r.drag_stopped() || (r.changed() && !r.dragged()) {
|
||||
Live::Released
|
||||
} else if r.changed() {
|
||||
Live::Dragging
|
||||
} else {
|
||||
Live::Idle
|
||||
};
|
||||
live_run(app, "Opacity", "transparency.set", json!({"opacity": o.round()}), phase);
|
||||
});
|
||||
});
|
||||
match widgets::opacity_blend(ui, "tr", op, blend, has) {
|
||||
Some(TransparencyEdit::Blend(b)) => {
|
||||
app.run("transparency.set", json!({"blend": b.label()})).ok();
|
||||
}
|
||||
Some(TransparencyEdit::Opacity(o, phase)) => live_run(app, "Opacity", "transparency.set", json!({ "opacity": o }), phase),
|
||||
None => {}
|
||||
}
|
||||
widgets::divider(ui);
|
||||
// Thumbnails and the opacity-mask controls.
|
||||
let hide_thumbs: bool = pstate(ui.ctx(), "tr-hide-thumbs");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Shared widgets in Illustrator's panel style. Colours come from theme tokens.
|
||||
|
||||
use egui::{Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, pos2, vec2};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_color::{BlendMode, Paint};
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
use crate::icons;
|
||||
|
|
@ -269,19 +269,125 @@ pub fn fill_stroke_proxy(ui: &mut Ui, fill: &Paint, stroke: &Paint, fill_active:
|
|||
|
||||
/// A compact dropdown. Returns the chosen index.
|
||||
pub fn dropdown(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, current: &str, options: &[&str], width: f32) -> Option<usize> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut chosen = None;
|
||||
let resp = egui::Frame::NONE.fill(t.input).stroke(Stroke::new(1.0, t.input_border)).corner_radius(CornerRadius::same(3)).show(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt(ui.id().with(id)).selected_text(egui::RichText::new(current).size(12.0)).width(width - 4.0).show_ui(ui, |ui| {
|
||||
for (i, o) in options.iter().enumerate() {
|
||||
if ui.selectable_label(*o == current, *o).clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
combo(ui, id, current, width, |ui| {
|
||||
let mut chosen = None;
|
||||
for (i, o) in options.iter().enumerate() {
|
||||
if ui.selectable_label(*o == current, *o).clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
/// The recessed combo box of [`dropdown`] showing `current`; `list` draws the options and returns
|
||||
/// the chosen one.
|
||||
fn combo<R>(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + std::fmt::Debug,
|
||||
current: &str,
|
||||
width: f32,
|
||||
list: impl FnOnce(&mut Ui) -> Option<R>,
|
||||
) -> Option<R> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::Frame::NONE
|
||||
.fill(t.input)
|
||||
.stroke(Stroke::new(1.0, t.input_border))
|
||||
.corner_radius(CornerRadius::same(3))
|
||||
.show(ui, |ui| {
|
||||
egui::ComboBox::from_id_salt(ui.id().with(id))
|
||||
.selected_text(egui::RichText::new(current).size(12.0))
|
||||
.width(width - 4.0)
|
||||
.show_ui(ui, list)
|
||||
.inner
|
||||
.flatten()
|
||||
})
|
||||
.inner
|
||||
}
|
||||
|
||||
/// Whether the blend-mode list draws a separator above `BlendMode::ALL[i]`: where a
|
||||
/// [`BlendMode::group`] starts.
|
||||
pub fn blend_separator_before(i: usize) -> bool {
|
||||
i > 0 && i < BlendMode::ALL.len() && BlendMode::ALL[i].group() != BlendMode::ALL[i - 1].group()
|
||||
}
|
||||
|
||||
/// The blend-mode dropdown, its groups separated (Normal | darken | lighten | contrast | inversion
|
||||
/// | component modes). Returns the chosen mode.
|
||||
pub fn blend_dropdown(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, current: BlendMode, width: f32) -> Option<BlendMode> {
|
||||
combo(ui, id, current.label(), width, |ui| {
|
||||
let mut chosen = None;
|
||||
for (i, m) in BlendMode::ALL.into_iter().enumerate() {
|
||||
if blend_separator_before(i) {
|
||||
ui.separator();
|
||||
}
|
||||
if ui.selectable_label(m == current, m.label()).clicked() {
|
||||
chosen = Some(m);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
/// The blend mode an effect parameter names (`"mode": "multiply"`), which editors show as a
|
||||
/// [`blend_param_dropdown`].
|
||||
pub fn blend_param(key: &str, value: &serde_json::Value) -> Option<BlendMode> {
|
||||
value.as_str().filter(|_| key == "mode").and_then(BlendMode::parse)
|
||||
}
|
||||
|
||||
/// [`blend_dropdown`] for a blend-mode parameter; returns the chosen mode's parameter value.
|
||||
pub fn blend_param_dropdown(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, current: BlendMode) -> Option<serde_json::Value> {
|
||||
blend_dropdown(ui, id, current, 120.0).map(|m| serde_json::Value::String(m.label().to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
/// An edit made with [`opacity_blend`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum TransparencyEdit {
|
||||
Blend(BlendMode),
|
||||
/// Opacity in percent, with the slider's drag phase (`Released` for a typed value).
|
||||
Opacity(f64, Live),
|
||||
}
|
||||
|
||||
/// The transparency controls shared by the Transparency panel and the Appearance panel's Opacity
|
||||
/// popups: the grouped blend-mode dropdown, the opacity percent field and its slider popup.
|
||||
pub fn opacity_blend(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + std::fmt::Debug + Copy,
|
||||
opacity: f32,
|
||||
blend: BlendMode,
|
||||
enabled: bool,
|
||||
) -> Option<TransparencyEdit> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut edit = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
if let Some(m) = blend_dropdown(ui, (id, "blend"), blend, 104.0) {
|
||||
edit = Some(TransparencyEdit::Blend(m));
|
||||
}
|
||||
});
|
||||
dim_label(ui, "Opacity:");
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.add_enabled_ui(enabled, |ui| {
|
||||
if let Some(o) = plain_field(ui, (id, "opacity"), opacity as f64 * 100.0, "%", 0, 50.0) {
|
||||
edit = Some(TransparencyEdit::Opacity(o.clamp(0.0, 100.0), Live::Released));
|
||||
}
|
||||
});
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(18.0, 26.0), if enabled { Sense::click() } else { Sense::hover() });
|
||||
ui.painter().rect_stroke(r, 2, Stroke::new(1.0, t.input_border), StrokeKind::Inside);
|
||||
icons::paint(ui, "chevron-right", r.shrink2(vec2(3.0, 7.0)), if enabled { t.icon } else { t.text_disabled });
|
||||
egui::Popup::menu(&resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
let mut o = opacity * 100.0;
|
||||
let r = ui.add(egui::Slider::new(&mut o, 0.0..=100.0).show_value(false));
|
||||
let phase = if r.drag_stopped() || (r.changed() && !r.dragged()) {
|
||||
Live::Released
|
||||
} else if r.changed() {
|
||||
Live::Dragging
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
edit = Some(TransparencyEdit::Opacity(o.round() as f64, phase));
|
||||
});
|
||||
});
|
||||
let _ = resp;
|
||||
chosen
|
||||
edit
|
||||
}
|
||||
|
||||
/// The 3×3 reference point locator. Returns a new index when clicked.
|
||||
|
|
|
|||
Loading…
Reference in New Issue