mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 05:16:56 -06:00
Merge pull request #789 from yelintong/feat/popover
M4: a generic popover — a 40-high header, dragged by its header
This commit is contained in:
@@ -32,6 +32,16 @@ impl FigApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clicking the control that opened the colour picker closes it again; clicking another opens
|
||||
/// the picker for that one.
|
||||
pub fn toggle_picker(&mut self, target: crate::colorpicker::PickerTarget, _top: Option<f32>) {
|
||||
if self.ui.picker.as_ref() == Some(&target) {
|
||||
crate::colorpicker::close(self);
|
||||
} else {
|
||||
self.ui.picker = Some(target);
|
||||
}
|
||||
}
|
||||
|
||||
/// Leaving Design ends a pen path as Escape does and goes back to Move: Prototype and Inspect
|
||||
/// don't draw (#307). Checked as the frame starts (the control channel) and after the panels
|
||||
/// and shortcuts (the mode switcher, the tabs, Shift+E/Shift+D), before the canvas sees more.
|
||||
|
||||
@@ -53,6 +53,25 @@ fn picker_closes_when_another_layer_is_selected() {
|
||||
assert!(u.app.ui.picker.is_none(), "selecting another layer closes the picker: {:?}", u.app.ui.picker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_the_swatch_again_closes_the_picker() {
|
||||
let (mut u, a, _) = two();
|
||||
u.app.run("fill.add", json!({"ids": [a], "color": "#00FF00"}));
|
||||
let target = PickerTarget::Paint { fills: true, index: 0, ids: vec![a] };
|
||||
u.app.toggle_picker(target.clone(), Some(200.0));
|
||||
u.frame(vec![]);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(u.app.ui.picker, Some(target.clone()), "the swatch opens the picker");
|
||||
u.app.toggle_picker(target.clone(), Some(200.0));
|
||||
u.frame(vec![]);
|
||||
assert!(u.app.ui.picker.is_none(), "the same swatch toggles it shut");
|
||||
// Another paint of the same layer opens the picker for that one.
|
||||
let second = PickerTarget::Paint { fills: true, index: 1, ids: vec![a] };
|
||||
u.app.toggle_picker(second.clone(), Some(200.0));
|
||||
u.frame(vec![]);
|
||||
assert_eq!(u.app.ui.picker, Some(second), "another paint opens the picker for it");
|
||||
}
|
||||
|
||||
// Regression test for #237: the "On this page" swatches wrapped onto rows that started at the picker's
|
||||
// left edge (only the first row was indented) and ran past its right edge.
|
||||
#[test]
|
||||
@@ -77,11 +96,9 @@ fn wrapped_page_colour_swatches_stay_inset_inside_the_picker() {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
let out = u.frame_output(vec![]);
|
||||
// The picker is the only floating window open.
|
||||
let picker = u
|
||||
.ctx
|
||||
.memory(|m| m.areas().visible_layer_ids().into_iter().filter(|l| l.order == egui::Order::Middle).find_map(|l| m.area_rect(l.id)))
|
||||
.expect("the picker is open");
|
||||
// The picker is the only floating window open: its area is the picker's own id, which the style
|
||||
// picker also looks up to tell a press in here from one outside.
|
||||
let picker = u.ctx.memory(|m| m.area_rect(crate::colorpicker::window_id())).expect("the picker is open");
|
||||
// A swatch is a 14 pt square with a 1 pt outline.
|
||||
let swatches: Vec<egui::Rect> = out
|
||||
.shapes
|
||||
|
||||
@@ -17,6 +17,7 @@ pub mod icons;
|
||||
pub mod live;
|
||||
pub mod menus;
|
||||
pub mod pending_open;
|
||||
pub mod popover;
|
||||
pub mod present;
|
||||
pub mod shortcuts;
|
||||
pub mod tabs;
|
||||
@@ -161,6 +162,8 @@ mod plugin_save_ui_tests;
|
||||
#[cfg(test)]
|
||||
mod plugins_browse_tests;
|
||||
#[cfg(test)]
|
||||
mod popover_tests;
|
||||
#[cfg(test)]
|
||||
mod popup_width_tests;
|
||||
#[cfg(test)]
|
||||
mod present_links_tests;
|
||||
@@ -402,6 +405,11 @@ pub struct UiState {
|
||||
/// The prototype preview pane floating over the canvas, when open.
|
||||
pub preview: Option<present::Pane>,
|
||||
pub picker: Option<colorpicker::PickerTarget>,
|
||||
/// The colour picker's popover while it is open (its place survives a drag).
|
||||
pub picker_popover: Option<popover::Popover>,
|
||||
/// The control that opened the picker, refreshed every frame it is drawn (the panel scrolls):
|
||||
/// a press in it is held out of the popover's closing, so its own click toggles the picker.
|
||||
pub picker_button: Option<egui::Rect>,
|
||||
pub context_menu: Option<(egui::Pos2, Vec<NodeId>)>,
|
||||
/// The context menu was asked for from the keyboard (`view.contextMenu`): it opens at the
|
||||
/// focused layer row or the canvas selection and takes the keyboard.
|
||||
@@ -449,6 +457,8 @@ pub struct UiState {
|
||||
pub color_profile: Option<figcraft_color::ColorProfile>,
|
||||
/// The open style picker.
|
||||
pub style_picker: Option<style_picker::Picker>,
|
||||
/// The style picker's popover while it is open (its place survives a drag).
|
||||
pub style_picker_popover: Option<popover::Popover>,
|
||||
/// The style picker shows colour styles as a grid of swatches.
|
||||
pub style_grid: bool,
|
||||
/// Style folders closed in the style picker.
|
||||
@@ -578,6 +588,8 @@ impl Default for UiState {
|
||||
present_view: present::PresentView::default(),
|
||||
preview: None,
|
||||
picker: None,
|
||||
picker_popover: None,
|
||||
picker_button: None,
|
||||
context_menu: None,
|
||||
key_menu: false,
|
||||
key_box: None,
|
||||
@@ -605,6 +617,7 @@ impl Default for UiState {
|
||||
pdf_print: figcraft_doc::PrintSettings::default(),
|
||||
color_profile: None,
|
||||
style_picker: None,
|
||||
style_picker_popover: None,
|
||||
style_grid: false,
|
||||
style_folds: BTreeSet::new(),
|
||||
text_preview: None,
|
||||
|
||||
@@ -192,3 +192,36 @@ fn adding_a_fill_to_mixed_fills_opens_no_picker() {
|
||||
assert_eq!((doc.node(a).unwrap().fills.len(), doc.node(b).unwrap().fills.len()), (2, 3));
|
||||
assert_eq!(u.app.ui.picker, None);
|
||||
}
|
||||
|
||||
/// The shared popover's closing reaches the colour picker: a press on the canvas closes it.
|
||||
#[test]
|
||||
fn a_press_on_the_canvas_closes_the_colour_picker() {
|
||||
let mut s = Session::new();
|
||||
let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap());
|
||||
s.execute("fill.add", &json!({"ids": [r], "color": "#FF0000"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.ctx.enable_accesskit();
|
||||
let sw = u.ctx.read_response(Id::unique(("swatch", true, 0usize))).unwrap().rect.center();
|
||||
click(&mut u, sw);
|
||||
assert!(u.app.ui.picker.is_some(), "the swatch opens the picker");
|
||||
click(&mut u, Pos2::new(320.0, 300.0));
|
||||
assert_eq!(u.app.ui.picker, None, "a press on the canvas closes the picker");
|
||||
}
|
||||
|
||||
/// Pressing the swatch that opened the picker again closes it: its press is held out of the
|
||||
/// popover's outside-press closing, so the swatch's own toggle gets the click.
|
||||
#[test]
|
||||
fn the_swatch_that_opened_the_colour_picker_closes_it_again() {
|
||||
let mut s = Session::new();
|
||||
let r = id(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap());
|
||||
s.execute("fill.add", &json!({"ids": [r], "color": "#FF0000"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.ctx.enable_accesskit();
|
||||
let sw = u.ctx.read_response(Id::unique(("swatch", true, 0usize))).unwrap().rect.center();
|
||||
click(&mut u, sw);
|
||||
assert!(u.app.ui.picker.is_some(), "the swatch opens the picker");
|
||||
click(&mut u, sw);
|
||||
assert_eq!(u.app.ui.picker, None, "the same swatch closes it again");
|
||||
}
|
||||
|
||||
@@ -306,11 +306,14 @@ fn page_section(app: &mut FigApp, ui: &mut Ui, doc: &Document) {
|
||||
};
|
||||
widgets::section_header(ui, tr("Page"), &[], false);
|
||||
let mut open_picker = false;
|
||||
let mut swatch_top = None;
|
||||
let mut new_hex = None;
|
||||
row(ui, 32.0, |ui| {
|
||||
let p = Paint::solid(bg);
|
||||
if widgets::swatch(ui, &p, 16.0).clicked() {
|
||||
let sw = widgets::swatch(ui, &p, 16.0);
|
||||
if sw.clicked() {
|
||||
open_picker = true;
|
||||
swatch_top = Some(sw.rect.min.y);
|
||||
}
|
||||
if let Some(h) = widgets::text_field(ui, Id::unique("pagebg"), &bg.to_hex(), 96.0, "") {
|
||||
new_hex = Some(h);
|
||||
@@ -318,7 +321,7 @@ fn page_section(app: &mut FigApp, ui: &mut Ui, doc: &Document) {
|
||||
widgets::label(ui, "100 %", t.text);
|
||||
});
|
||||
if open_picker {
|
||||
app.ui.picker = Some(crate::colorpicker::PickerTarget::PageBackground);
|
||||
app.toggle_picker(crate::colorpicker::PickerTarget::PageBackground, swatch_top);
|
||||
}
|
||||
if let Some(h) = new_hex
|
||||
&& let Some(c) = figcraft_color::Color::from_hex_or_name(&h)
|
||||
|
||||
@@ -329,8 +329,9 @@ fn tweaked(e: &Effect, f: impl FnOnce(&mut EffectKind)) -> Effect {
|
||||
/// duo noise's second colour), in the current row; `edit` applies a new one.
|
||||
fn fx_color(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, e: &Effect, secondary: bool, edit: &ColorEdit<'_>) {
|
||||
let Some(c) = e.color(secondary) else { return };
|
||||
if widgets::swatch(ui, &Paint::solid(c), 16.0).clicked() {
|
||||
app.ui.picker = Some(crate::colorpicker::PickerTarget::Effect { ids: ids.to_vec(), index: i, secondary });
|
||||
let sw = widgets::swatch(ui, &Paint::solid(c), 16.0);
|
||||
if sw.clicked() {
|
||||
app.toggle_picker(crate::colorpicker::PickerTarget::Effect { ids: ids.to_vec(), index: i, secondary }, Some(sw.rect.min.y));
|
||||
}
|
||||
if let Some(h) = widgets::text_field(ui, Id::unique(("fx_c", i, secondary)), &c.to_hex(), 80.0, "")
|
||||
&& let Some(nc) = figcraft_color::Color::from_hex_or_name(&h)
|
||||
|
||||
@@ -253,8 +253,9 @@ fn range_fill(app: &mut FigApp, ui: &mut Ui, doc: &Document, id: NodeId, start:
|
||||
match fills.as_ref().and_then(|f| f.first()) {
|
||||
Some(p) => {
|
||||
widgets::paint_swatch(ui.painter(), sw, p);
|
||||
if ui.interact(sw.expand(2.0), Id::unique("range_swatch"), Sense::click()).clicked() {
|
||||
app.ui.picker = Some(crate::colorpicker::PickerTarget::TextRange { id, start, end });
|
||||
let range_swatch = ui.interact(sw.expand(2.0), Id::unique("range_swatch"), Sense::click());
|
||||
if range_swatch.clicked() {
|
||||
app.toggle_picker(crate::colorpicker::PickerTarget::TextRange { id, start, end }, Some(range_swatch.rect.min.y));
|
||||
}
|
||||
if let Some(c) = p.solid_color() {
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_r));
|
||||
@@ -445,8 +446,17 @@ fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint,
|
||||
let sw = Rect::from_min_size(pos2(r.min.x + 4.0, r.center().y - 8.0), vec2(16.0, 16.0));
|
||||
widgets::paint_swatch(ui.painter(), sw, p);
|
||||
let sresp = ui.interact(sw.expand(2.0), Id::unique(("swatch", fills, i)), Sense::click());
|
||||
// While this paint is the one the picker edits, its swatch is held out of the popover's
|
||||
// closing, so a press on it again reaches the toggle below.
|
||||
let editing = matches!(
|
||||
&app.ui.picker,
|
||||
Some(crate::colorpicker::PickerTarget::Paint { fills: f, index: k, ids: held }) if *f == fills && *k == i && *held == ids
|
||||
);
|
||||
if editing {
|
||||
app.ui.picker_button = Some(sresp.rect);
|
||||
}
|
||||
if sresp.clicked() {
|
||||
app.ui.picker = Some(crate::colorpicker::PickerTarget::Paint { fills, index: i, ids: ids.clone() });
|
||||
app.toggle_picker(crate::colorpicker::PickerTarget::Paint { fills, index: i, ids: ids.clone() }, Some(sresp.rect.min.y));
|
||||
}
|
||||
let text_r = Rect::from_min_max(pos2(sw.max.x + 8.0, r.min.y), pos2(r.max.x - 52.0, r.max.y));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_r));
|
||||
|
||||
@@ -47,9 +47,9 @@ fn colors(ui: &Ui, document: u64, doc: &Arc<Document>, ids: &[NodeId], interacti
|
||||
colors
|
||||
}
|
||||
|
||||
pub(crate) fn open_picker(app: &mut FigApp, targets: Vec<PaintTarget>) {
|
||||
pub(crate) fn toggle(app: &mut FigApp, targets: Vec<PaintTarget>, top: Option<f32>) {
|
||||
let Ok(ds) = app.session.ds() else { return };
|
||||
app.ui.picker = Some(PickerTarget::SelectionColor { targets, selection: ds.selection.clone(), document: ds.uid });
|
||||
app.toggle_picker(PickerTarget::SelectionColor { targets, selection: ds.selection.clone(), document: ds.uid }, top);
|
||||
}
|
||||
|
||||
/// Draw the selection-colour groups (at least two): swatch opens the picker, hex and opacity
|
||||
@@ -72,7 +72,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, ids: &[NodeId
|
||||
crate::right::row(ui, 32.0, |ui| {
|
||||
let swatch = widgets::swatch(ui, &group.paint, 24.0).tip(tr("Edit colour"));
|
||||
if swatch.clicked() {
|
||||
open_picker(app, group.targets.clone());
|
||||
toggle(app, group.targets.clone(), Some(swatch.rect.min.y));
|
||||
}
|
||||
let width = (ui.available_width() - 116.0).max(40.0);
|
||||
let linked_name = group
|
||||
@@ -82,7 +82,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, ids: &[NodeId
|
||||
.or_else(|| group.variable.as_ref().and_then(|id| doc.variables.get(id).map(|v| v.name.as_str())));
|
||||
if let Some(name) = linked_name {
|
||||
if widgets::dropdown(ui, name, width, None).clicked() {
|
||||
open_picker(app, group.targets.clone());
|
||||
toggle(app, group.targets.clone(), None);
|
||||
}
|
||||
} else if let Some(color) = group.paint.solid_color() {
|
||||
if let Some(hex) = widgets::text_field_named(ui, ui.scope_id().with("hex"), &color.to_hex(), width, tr("Color")) {
|
||||
@@ -100,7 +100,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, ids: &[NodeId
|
||||
} else {
|
||||
let response = widgets::dropdown(ui, &widgets::paint_name(&group.paint), width, None);
|
||||
if response.clicked() {
|
||||
open_picker(app, group.targets.clone());
|
||||
toggle(app, group.targets.clone(), Some(response.rect.min.y));
|
||||
}
|
||||
}
|
||||
let alpha = format!("{}%", figcraft_doc::trim_num(f64::from(group.paint.opacity) * 100.0));
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
//! The chrome every detached panel shares: a card with a header — the title 16 points in from its
|
||||
//! left edge, a 24-point X 8 in from its right — a hairline under the header and the theme's popup
|
||||
//! shadow, around one slot.
|
||||
//!
|
||||
//! The measurements are our own: they are the header the colour picker has always drawn, and the
|
||||
//! card is as wide as it. The shadow comes from the theme, so the dark theme gets its own. Nothing
|
||||
//! here is taken from another product's interface.
|
||||
//!
|
||||
//! It opens against an anchor (its right edge on it, its top on it, lifted so its bottom keeps off
|
||||
//! the screen's bottom) or at a point, the header drags it, and it closes itself on the X, on
|
||||
//! Escape and on a press outside it — except in the rects the caller holds out (the button that
|
||||
//! opened it, a popover it opened in turn), so that button's own click can toggle it shut instead
|
||||
//! of the press closing it and the release opening it again.
|
||||
|
||||
use egui::epaint::RectShape;
|
||||
use egui::{Align2, Color32, Context, CornerRadius, Id, Pos2, Rect, Sense, Ui, Vec2, pos2, vec2};
|
||||
|
||||
use crate::a11y::Tip;
|
||||
use crate::i18n::tr;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{Tokens, semibold};
|
||||
use crate::widgets;
|
||||
|
||||
/// The default width: the colour picker's, and the one every detached panel shares.
|
||||
pub const WIDTH: f32 = 240.0;
|
||||
/// The header's height: the title's line and the X sit in it.
|
||||
pub const HEADER_H: f32 = 40.0;
|
||||
/// The title starts this far from the panel's left edge, the X ends this far from its right one.
|
||||
const TITLE_LEFT: f32 = 16.0;
|
||||
const CLOSE_RIGHT: f32 = 8.0;
|
||||
/// The X's button (the icon is inset in it).
|
||||
const CLOSE: f32 = 24.0;
|
||||
/// The slot's default margin inside the panel.
|
||||
const SLOT_PAD: f32 = 16.0;
|
||||
/// A lifted popover keeps this much screen under it; it stays [`EDGE`] inside the others.
|
||||
const SCREEN_GAP: f32 = 16.0;
|
||||
const EDGE: f32 = 8.0;
|
||||
/// The panel's corners.
|
||||
const RADIUS: u8 = 13;
|
||||
|
||||
/// The surface a popover draws on: the panels' own, or the dark one the style picker uses.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Surface {
|
||||
Panel,
|
||||
Menu,
|
||||
}
|
||||
|
||||
/// Where a popover opens when the caller has not put it down itself.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Open {
|
||||
/// Its right edge on `x` (the right panel's left edge), its top on `y` (the button's top),
|
||||
/// lifted when that would run its bottom past the screen's bottom.
|
||||
Anchor(Pos2),
|
||||
/// Its top-left exactly here.
|
||||
At(Pos2),
|
||||
}
|
||||
|
||||
/// What keeps a popover open while a press or Escape happens somewhere else.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Hold<'a> {
|
||||
/// Presses in these rects do not close it: the button that opened it, the popovers it opened.
|
||||
pub rects: &'a [Rect],
|
||||
/// Escape belongs to something above it (a field being typed in, a picker of its own).
|
||||
pub escape: bool,
|
||||
}
|
||||
|
||||
/// Where the popover shows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Place {
|
||||
/// From [`Open`], until the header drags it.
|
||||
Auto,
|
||||
/// Here: put down by the caller or by the drag.
|
||||
At(Pos2),
|
||||
}
|
||||
|
||||
/// An open popover.
|
||||
pub struct Popover {
|
||||
id: Id,
|
||||
/// The header's line.
|
||||
pub title: String,
|
||||
width: f32,
|
||||
pad: f32,
|
||||
surface: Surface,
|
||||
/// Whether a press outside it closes it.
|
||||
outside: bool,
|
||||
place: Place,
|
||||
/// While the header is held down: where in the panel the pointer grabbed it.
|
||||
grab: Option<Vec2>,
|
||||
/// The panel, its header, its slot and the X, as drawn last frame.
|
||||
pub rect: Rect,
|
||||
pub header: Rect,
|
||||
pub slot: Rect,
|
||||
pub close: Rect,
|
||||
/// The panel's size as laid out last frame.
|
||||
size: Vec2,
|
||||
}
|
||||
|
||||
impl Popover {
|
||||
/// A popover titled `title`; `id` names its egui area, so it must be stable and unique.
|
||||
pub fn new(id: impl std::hash::Hash + std::fmt::Debug, title: impl Into<String>) -> Self {
|
||||
Self::with_id(Id::unique(id), title)
|
||||
}
|
||||
|
||||
/// A popover whose egui area is `id` itself — not a hash of it — so a caller that publishes the
|
||||
/// id can find the area again (the colour picker's [`crate::colorpicker::window_id`]).
|
||||
pub fn with_id(id: Id, title: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
title: title.into(),
|
||||
width: WIDTH,
|
||||
pad: SLOT_PAD,
|
||||
surface: Surface::Panel,
|
||||
outside: true,
|
||||
place: Place::Auto,
|
||||
grab: None,
|
||||
rect: Rect::NOTHING,
|
||||
header: Rect::NOTHING,
|
||||
slot: Rect::NOTHING,
|
||||
close: Rect::NOTHING,
|
||||
size: vec2(WIDTH, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// `width` points wide instead of [`WIDTH`].
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.width = width.max(1.0);
|
||||
self.size.x = self.width;
|
||||
self
|
||||
}
|
||||
|
||||
/// `pad` points of margin around the slot instead of the default 16.
|
||||
pub fn pad(mut self, pad: f32) -> Self {
|
||||
self.pad = pad.max(0.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Drawn on the dark surface the style browser uses.
|
||||
pub fn surface(mut self, surface: Surface) -> Self {
|
||||
self.surface = surface;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether a press outside it closes it (the default). The colour picker stays open, so a
|
||||
/// press on a row beside it keeps editing the paint it is on.
|
||||
pub fn close_on_outside(mut self, yes: bool) -> Self {
|
||||
self.outside = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Its top-left at `pos`, instead of where [`Open`] would put it.
|
||||
pub fn at(mut self, pos: Pos2) -> Self {
|
||||
self.place = Place::At(pos);
|
||||
self
|
||||
}
|
||||
|
||||
/// Draws the panel and fills its slot with `body`; true when it asked to close — the X,
|
||||
/// Escape or a press outside it (outside [`Hold`]'s rects) on this frame.
|
||||
pub fn show(&mut self, ctx: &Context, open: Open, hold: Hold<'_>, body: impl FnOnce(&mut Ui)) -> bool {
|
||||
let t = Tokens::get(ctx);
|
||||
let dark = self.surface == Surface::Menu;
|
||||
let screen = ctx.content_rect();
|
||||
// The anchor rule needs the panel's height, which only a layout gives: the first frame
|
||||
// lays it out invisibly and the next one places it — a jump would show.
|
||||
let first = self.size.y <= 0.0;
|
||||
let pos = match self.place {
|
||||
Place::At(p) => clamp(p, self.size, screen),
|
||||
Place::Auto => {
|
||||
let p = match open {
|
||||
Open::At(p) => p,
|
||||
Open::Anchor(a) => open_pos(a, self.size, screen),
|
||||
};
|
||||
clamp(p, self.size, screen)
|
||||
}
|
||||
};
|
||||
let mut close = false;
|
||||
egui::Area::new(self.id).order(egui::Order::Foreground).fixed_pos(pos).show(ctx, |ui| {
|
||||
if first {
|
||||
ui.set_invisible();
|
||||
ctx.request_repaint();
|
||||
}
|
||||
// The panel is painted into a place kept before the content: the shadow and the fill
|
||||
// go under it, though its size is only known once the slot is laid out.
|
||||
let bg = ui.painter().add(egui::Shape::Noop);
|
||||
let spacing = ui.spacing().item_spacing;
|
||||
ui.spacing_mut().item_spacing = Vec2::ZERO;
|
||||
// 1. The header: the title, then the X at the right.
|
||||
let (header, _) = ui.allocate_exact_size(vec2(self.width, HEADER_H), Sense::hover());
|
||||
let close_rect = Rect::from_min_size(pos2(header.max.x - CLOSE_RIGHT - CLOSE, header.center().y - CLOSE / 2.0), Vec2::splat(CLOSE));
|
||||
let handle = Rect::from_min_max(header.min, pos2(close_rect.min.x - 6.0, header.max.y));
|
||||
let drag = ui.interact(handle, self.id.with("drag"), Sense::drag());
|
||||
let font = semibold(11.0);
|
||||
let title = widgets::elide_font(ui, &self.title, (handle.width() - TITLE_LEFT - 8.0).max(0.0), font.clone());
|
||||
ui.painter().text(pos2(header.min.x + TITLE_LEFT, header.center().y), Align2::LEFT_CENTER, title, font, header_text(t, dark));
|
||||
close = close_button(ui, self.id.with("close"), close_rect, dark, t);
|
||||
// 2. The header drags the panel (grabbed where the press landed, so the first frame of
|
||||
// a fast drag counts too); the X keeps its own clicks.
|
||||
if drag.hovered() && ctx.input(|i| i.pointer.primary_pressed()) {
|
||||
self.grab = ctx.input(|i| i.pointer.interact_pos()).map(|at| at - pos);
|
||||
}
|
||||
if drag.dragged()
|
||||
&& let (Some(grab), Some(at)) = (self.grab, drag.interact_pointer_pos())
|
||||
{
|
||||
self.place = Place::At(clamp(at - grab, self.size, screen));
|
||||
}
|
||||
if ctx.input(|i| i.pointer.any_released()) {
|
||||
self.grab = None;
|
||||
}
|
||||
if drag.dragged() || drag.hovered() {
|
||||
ui.ctx().set_cursor_icon(if drag.dragged() { egui::CursorIcon::Grabbing } else { egui::CursorIcon::Grab });
|
||||
}
|
||||
// 3. The slot, under a hairline.
|
||||
let frame = egui::Frame::NONE.inner_margin(egui::Margin::same(self.pad as i8)).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = spacing;
|
||||
ui.set_width(self.width - 2.0 * self.pad);
|
||||
body(ui);
|
||||
ui.min_rect()
|
||||
});
|
||||
let slot = frame.inner;
|
||||
let panel = Rect::from_min_size(header.min, vec2(self.width, frame.response.rect.max.y - header.min.y));
|
||||
let shadow = ctx.global_style().visuals.popup_shadow;
|
||||
let fill = if dark { t.menu } else { t.panel };
|
||||
let shapes = vec![
|
||||
egui::Shape::from(shadow.as_shape(panel, RADIUS)),
|
||||
RectShape::filled(panel, CornerRadius::same(RADIUS), fill).into(),
|
||||
// A point of the theme's border colour under the header.
|
||||
RectShape::filled(
|
||||
Rect::from_min_max(pos2(panel.min.x, header.max.y - 1.0), pos2(panel.max.x, header.max.y)),
|
||||
CornerRadius::ZERO,
|
||||
if dark { Color32::from_white_alpha(24) } else { t.border },
|
||||
)
|
||||
.into(),
|
||||
];
|
||||
ui.painter().set(bg, egui::Shape::Vec(shapes));
|
||||
self.rect = panel;
|
||||
self.header = header;
|
||||
self.slot = slot;
|
||||
self.close = close_rect;
|
||||
self.size = panel.size();
|
||||
});
|
||||
// Closing is the widget's: the X above, Escape and a press outside (a child popup or a
|
||||
// held-out rect keeps it open, so the button that opened it can click it shut).
|
||||
let popup_open = egui::Popup::is_any_open(ctx);
|
||||
let escape = !hold.escape && !popup_open && ctx.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
let outside = self.outside
|
||||
&& !popup_open
|
||||
&& ctx.input(|i| {
|
||||
i.pointer.any_pressed()
|
||||
&& i.pointer.interact_pos().is_some_and(|at| !self.rect.contains(at) && !hold.rects.iter().any(|r| r.contains(at)))
|
||||
});
|
||||
close || escape || outside
|
||||
}
|
||||
}
|
||||
|
||||
fn header_text(t: Tokens, dark: bool) -> Color32 {
|
||||
if dark { t.menu_text } else { t.text }
|
||||
}
|
||||
|
||||
/// The X: a 24-point button, its icon inset 6 (the colour picker's header).
|
||||
fn close_button(ui: &mut Ui, id: Id, rect: Rect, dark: bool, t: Tokens) -> bool {
|
||||
let resp = ui.interact(rect, id, Sense::click()).tip(tr("Close"));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), if dark { Color32::from_white_alpha(20) } else { t.hover });
|
||||
}
|
||||
icons::paint(ui.painter(), rect.shrink(6.0), Icon::Close, if dark { t.menu_text } else { t.icon });
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
/// The opening position: the right edge on `anchor.x` (the right panel's left edge), the top on
|
||||
/// `anchor.y` (the button's top) — lifted, when that runs its bottom past the screen's bottom, to
|
||||
/// keep [`SCREEN_GAP`] under it.
|
||||
fn open_pos(anchor: Pos2, size: Vec2, screen: Rect) -> Pos2 {
|
||||
let bottom = screen.max.y - SCREEN_GAP;
|
||||
let y = if anchor.y + size.y > bottom { bottom - size.y } else { anchor.y };
|
||||
pos2(anchor.x - size.x, y)
|
||||
}
|
||||
|
||||
/// Keeps the panel inside the screen: whole when it fits, its top-left corner when it doesn't.
|
||||
fn clamp(pos: Pos2, size: Vec2, screen: Rect) -> Pos2 {
|
||||
let (x0, y0) = (screen.min.x + EDGE, screen.min.y + EDGE);
|
||||
let x = pos.x.clamp(x0, (screen.max.x - size.x - EDGE).max(x0));
|
||||
let y = pos.y.clamp(y0, (screen.max.y - size.y - EDGE).max(y0));
|
||||
pos2(x, y)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! The shared popover drawn on its own: 240 points wide, a 40-high header with the title 16 in
|
||||
//! from the left and the X 8 in from the right, the slot under a hairline, the opening rule
|
||||
//! against an anchor (right edge on it, top aligned, lifted when that would run off the screen),
|
||||
//! the header dragging it, and closing on the X, Escape and a press outside — but not on a press
|
||||
//! in a held-out rect, so the button that opened it can toggle it shut.
|
||||
|
||||
use egui::{Event, Key, PointerButton, Pos2, RawInput, Rect, pos2, vec2};
|
||||
|
||||
use crate::popover::{HEADER_H, Hold, Open, Popover, WIDTH};
|
||||
|
||||
const SCREEN: egui::Vec2 = vec2(1440.0, 900.0);
|
||||
|
||||
/// A context with the UI's fonts: the header's title is drawn in the semibold family.
|
||||
fn context() -> egui::Context {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx, false, 0);
|
||||
ctx
|
||||
}
|
||||
|
||||
fn input(t: &mut f64, events: Vec<Event>) -> RawInput {
|
||||
*t += 1.0 / 60.0;
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, SCREEN)), time: Some(*t), events, ..Default::default() }
|
||||
}
|
||||
|
||||
fn press(p: Pos2, pressed: bool) -> Vec<Event> {
|
||||
vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() }]
|
||||
}
|
||||
|
||||
fn escape() -> Vec<Event> {
|
||||
vec![Event::Key { key: Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]
|
||||
}
|
||||
|
||||
/// One frame of `p` opened at `open`, its slot holding a row: the shapes it painted and whether it
|
||||
/// asked to close.
|
||||
fn draw(ctx: &egui::Context, t: &mut f64, p: &mut Popover, open: Open, hold: Hold<'_>, events: Vec<Event>) -> (Vec<egui::Shape>, bool) {
|
||||
let mut closed = false;
|
||||
let mut out = ctx.run_ui(input(t, events), |ui| {
|
||||
closed = p.show(ui.ctx(), open, hold, |ui| {
|
||||
ui.label("slot");
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
(crate::live_tests::flat(&out.shapes), closed)
|
||||
}
|
||||
|
||||
/// Two frames: the first lays the panel out to learn its height, the second places and paints it.
|
||||
fn settle(ctx: &egui::Context, t: &mut f64, p: &mut Popover, open: Open) -> Vec<egui::Shape> {
|
||||
draw(ctx, t, p, open, Hold::default(), vec![]);
|
||||
draw(ctx, t, p, open, Hold::default(), vec![]).0
|
||||
}
|
||||
|
||||
fn near(a: Pos2, b: Pos2) -> bool {
|
||||
(a - b).length() < 0.05
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_header_is_40_high_with_the_title_16_in_and_the_x_8_in() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
let mut p = Popover::new("popover_header", "Custom");
|
||||
let shapes = settle(&ctx, &mut t, &mut p, Open::At(pos2(160.0, 100.0)));
|
||||
assert_eq!(p.rect.width(), WIDTH, "240 points wide");
|
||||
assert!(near(p.rect.min, pos2(160.0, 100.0)), "where it was put: {:?}", p.rect);
|
||||
assert_eq!(p.header.height(), HEADER_H, "the header is 40 high");
|
||||
// The X: 24 square, its right edge 8 in from the panel's, centred on the header's line.
|
||||
assert!((p.close.max.x - (p.rect.max.x - 8.0)).abs() < 0.01, "the X is 8 in from the right: {:?}", p.close);
|
||||
assert!((p.close.center().y - p.header.center().y).abs() < 0.01, "the X is on the header's line");
|
||||
// The title: one line, 16 in from the left, on the header's middle.
|
||||
let (_, at, size) = crate::live_tests::texts(&shapes).into_iter().find(|(s, _, _)| s == "Custom").expect("the title shows");
|
||||
assert!((at.x - (p.rect.min.x + 16.0)).abs() < 0.5, "the title starts 16 in: {at:?}");
|
||||
assert!((at.y + size.y / 2.0 - p.header.center().y).abs() < 1.0, "the title is on the header's line: {at:?}");
|
||||
// The slot: 16 in from the panel's sides, under the header.
|
||||
assert!((p.slot.min.x - (p.rect.min.x + 16.0)).abs() < 0.01, "the slot starts 16 in: {:?}", p.slot);
|
||||
assert!((p.slot.min.y - (p.header.max.y + 16.0)).abs() < 0.01, "the slot starts under the header: {:?}", p.slot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_opens_with_its_right_edge_on_the_anchor_and_its_top_aligned() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
let mut p = Popover::new("popover_anchor", "Popup");
|
||||
settle(&ctx, &mut t, &mut p, Open::Anchor(pos2(900.0, 200.0)));
|
||||
assert!((p.rect.max.x - 900.0).abs() < 0.01, "its right edge on the anchor: {:?}", p.rect);
|
||||
assert!((p.rect.min.y - 200.0).abs() < 0.01, "its top on the anchor: {:?}", p.rect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_lifts_so_its_bottom_keeps_16_from_the_screens_bottom() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
let mut p = Popover::new("popover_lift", "Popup");
|
||||
settle(&ctx, &mut t, &mut p, Open::Anchor(pos2(900.0, 860.0)));
|
||||
// Aligning the top would run it off: it rises so its bottom sits 16 above the screen's.
|
||||
assert!((p.rect.max.y - (SCREEN.y - 16.0)).abs() < 0.01, "its bottom is 16 above the screen's: {:?}", p.rect);
|
||||
assert!(p.rect.min.y < 860.0, "it was lifted: {:?}", p.rect);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_header_drags_it_and_the_slot_does_not() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
let mut p = Popover::new("popover_drag", "Popup");
|
||||
settle(&ctx, &mut t, &mut p, Open::At(pos2(160.0, 100.0)));
|
||||
let at = Open::At(pos2(160.0, 100.0));
|
||||
let (from, to) = (pos2(200.0, 120.0), pos2(250.0, 180.0));
|
||||
let before = p.rect.min;
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), press(from, true));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![Event::PointerMoved(to)]);
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![]);
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), press(to, false));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![]);
|
||||
assert!(near(p.rect.min, before + (to - from)), "the header dragged it by the pointer's move: {:?} from {before:?}", p.rect.min);
|
||||
// The slot does not drag it.
|
||||
let (from, to) = (p.slot.center(), p.slot.center() + vec2(-70.0, -40.0));
|
||||
let held = p.rect.min;
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), press(from, true));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![Event::PointerMoved(to)]);
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), press(to, false));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![]);
|
||||
assert!(near(p.rect.min, held), "a press in the slot leaves it: {:?} from {held:?}", p.rect.min);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_x_and_escape_close_it() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
let mut p = Popover::new("popover_close", "Popup");
|
||||
settle(&ctx, &mut t, &mut p, Open::At(pos2(160.0, 100.0)));
|
||||
let at = Open::At(pos2(160.0, 100.0));
|
||||
let close = p.close.center();
|
||||
assert!(!draw(&ctx, &mut t, &mut p, at, Hold::default(), press(close, true)).1, "a press is not a click yet");
|
||||
assert!(draw(&ctx, &mut t, &mut p, at, Hold::default(), press(close, false)).1, "the X asks for the close");
|
||||
assert!(draw(&ctx, &mut t, &mut p, at, Hold::default(), escape()).1, "Escape asks for the close");
|
||||
// Escape that belongs to something above it does not.
|
||||
let mut p = Popover::new("popover_escape_held", "Popup");
|
||||
settle(&ctx, &mut t, &mut p, Open::At(pos2(160.0, 100.0)));
|
||||
let hold = Hold { rects: &[], escape: true };
|
||||
assert!(!draw(&ctx, &mut t, &mut p, at, hold, escape()).1, "a held Escape leaves it open");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_outside_closes_it_but_not_on_the_button_that_opened_it() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
let mut p = Popover::new("popover_outside", "Popup");
|
||||
settle(&ctx, &mut t, &mut p, Open::At(pos2(300.0, 200.0)));
|
||||
let at = Open::At(pos2(300.0, 200.0));
|
||||
// The button that opened it: a press there leaves it open, so its release can toggle it shut.
|
||||
let opener = Rect::from_min_size(pos2(60.0, 60.0), vec2(24.0, 24.0));
|
||||
let hold = Hold { rects: &[opener], escape: false };
|
||||
let on_opener = opener.center();
|
||||
assert!(!draw(&ctx, &mut t, &mut p, at, hold, press(on_opener, true)).1, "a press on the opener leaves it open");
|
||||
assert!(!draw(&ctx, &mut t, &mut p, at, hold, press(on_opener, false)).1, "and so does its release");
|
||||
// A press anywhere else closes it, before the release.
|
||||
let elsewhere = pos2(900.0, 400.0);
|
||||
assert!(draw(&ctx, &mut t, &mut p, at, hold, press(elsewhere, true)).1, "a press outside closes it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_caller_can_put_it_where_it_likes() {
|
||||
let ctx = context();
|
||||
let mut t = 0.0;
|
||||
// Placed outright, the opening rule no longer matters.
|
||||
let mut p = Popover::new("popover_at", "Popup").at(pos2(300.0, 250.0));
|
||||
settle(&ctx, &mut t, &mut p, Open::At(pos2(900.0, 100.0)));
|
||||
assert!(near(p.rect.min, pos2(300.0, 250.0)), "where the caller put it: {:?}", p.rect);
|
||||
// Nor after a drag: it stays where it was dropped.
|
||||
let at = Open::At(pos2(900.0, 100.0));
|
||||
let (from, to) = (pos2(340.0, 270.0), pos2(400.0, 320.0));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), press(from, true));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![Event::PointerMoved(to)]);
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), press(to, false));
|
||||
draw(&ctx, &mut t, &mut p, at, Hold::default(), vec![]);
|
||||
assert!(near(p.rect.min, pos2(360.0, 300.0)), "dropped where the drag left it: {:?}", p.rect);
|
||||
}
|
||||
@@ -454,6 +454,11 @@ fn key_event(app: &mut FigApp, key: Key, physical: Option<Key>, m: Modifiers, re
|
||||
if key == Key::Escape && crate::selection_spacing::cancel(app, ctx) {
|
||||
return true;
|
||||
}
|
||||
// The colour picker open: Escape closes it here, before the binding deselects — unless one of
|
||||
// its own dropdowns is open, which takes this Escape.
|
||||
if key == Key::Escape && !egui::Popup::is_any_open(ctx) && crate::colorpicker::close_if_open(app) {
|
||||
return true;
|
||||
}
|
||||
if key_box(app, key, m) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The colour picker popover: paint type, saturation/brightness square, hue and alpha sliders,
|
||||
//! hex and RGB fields, gradient stops, image and pattern fills and document colours.
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Id, Mesh, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use egui::{Align2, Color32, CornerRadius, Id, Mesh, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use figcraft_color::{
|
||||
BlendMode, Color, ColorStop, Gradient, GradientKind, Hsb, ImageFilters, ImagePaint, ImageScaleMode, Paint, PaintKind, PatternAlign, PatternPaint,
|
||||
PatternTile,
|
||||
@@ -13,7 +13,8 @@ use crate::FigApp;
|
||||
use crate::a11y::Tip;
|
||||
use crate::i18n::{tr, tr_noop, trf};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{Tokens, doc_c32, regular, semibold};
|
||||
use crate::popover::{Hold, Open, Popover};
|
||||
use crate::theme::{Tokens, doc_c32, regular};
|
||||
use crate::widgets;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -853,6 +854,7 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
finish(app);
|
||||
}
|
||||
app.ui.picker = None;
|
||||
app.ui.picker_popover = None;
|
||||
return;
|
||||
};
|
||||
let t = Tokens::get(ctx);
|
||||
@@ -863,170 +865,184 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
_ => None,
|
||||
};
|
||||
let pos = beside.unwrap_or(pos2(screen.max.x - app.ui.right_width - 260.0, 140.0));
|
||||
let mut open = true;
|
||||
let mut close_now = false;
|
||||
egui::Window::new("color_picker")
|
||||
// A fixed area id so the style picker can tell a click in here from a click outside
|
||||
// (`Window::new` alone would key the area by `Id::unique(Some(title))`).
|
||||
.id(window_id())
|
||||
.title_bar(false)
|
||||
.fixed_pos(pos)
|
||||
.fixed_size(vec2(240.0, 0.0))
|
||||
.frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(0)))
|
||||
.open(&mut open)
|
||||
.show(ctx, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
|
||||
// The eyedropper's colour goes to this picker's paint (a stroke, an effect, a gradient stop...).
|
||||
if let Some(c) = app.session.tool.picked.take() {
|
||||
set_color(app, &target, &mut paint, c, stop_id(), ui);
|
||||
// The colour picker's own header — title 16 in, the X 8 in from the right — is the shared
|
||||
// popover's chrome now; the picker keeps its body. Its area id is [`window_id`], the one the
|
||||
// style picker looks up to tell a press in here from one outside.
|
||||
let mut pop = app.ui.picker_popover.take().unwrap_or_else(|| Popover::with_id(window_id(), tr("Custom")).pad(0.0));
|
||||
// Sampling a colour from the canvas — the eyedropper this picker's own button started, or the
|
||||
// colour it just sampled — is the press, not a press outside: neither may close the picker.
|
||||
let picking = app.session.tool.pick_only || app.session.tool.picked.is_some();
|
||||
pop = pop.close_on_outside(!picking);
|
||||
// Closing is the shared widget's: the X, Escape and a press outside it. Presses in the panel
|
||||
// beside it keep it (swatches and fields there switch the colour it edits); the control that
|
||||
// opened it is held too, so pressing it again reaches its own toggle.
|
||||
let panel = Rect::from_min_max(pos2(screen.max.x - app.ui.right_width, screen.min.y), screen.max);
|
||||
let mut held: Vec<Rect> = vec![panel];
|
||||
held.extend(app.ui.picker_button);
|
||||
let closed = pop.show(ctx, Open::At(pos), Hold { rects: &held, escape: false }, |ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
|
||||
// The eyedropper's colour goes to this picker's paint (a stroke, an effect, a gradient stop...).
|
||||
if let Some(c) = app.session.tool.picked.take() {
|
||||
set_color(app, &target, &mut paint, c, stop_id(), ui);
|
||||
}
|
||||
// Paint type.
|
||||
if !matches!(
|
||||
target,
|
||||
PickerTarget::Effect { .. } | PickerTarget::PageBackground | PickerTarget::Variable { .. } | PickerTarget::PluginField { .. }
|
||||
) {
|
||||
paint_type_row(ui, app, &target, &mut paint);
|
||||
}
|
||||
// The paint's own blend mode, with the paints and layers under it.
|
||||
if !matches!(
|
||||
target,
|
||||
PickerTarget::Effect { .. } | PickerTarget::PageBackground | PickerTarget::Variable { .. } | PickerTarget::PluginField { .. }
|
||||
) {
|
||||
blend_row(ui, app, &target, &mut paint);
|
||||
}
|
||||
// Gradient stops.
|
||||
let mut edit_color: Option<Color> = match &paint.kind {
|
||||
PaintKind::Solid { color } => Some(color.with_alpha(color.a * paint.opacity)),
|
||||
_ => None,
|
||||
};
|
||||
let sel_stop_id = stop_id();
|
||||
if let PaintKind::Gradient(g) = paint.kind.clone() {
|
||||
edit_color = gradient_controls(app, ui, &target, &mut paint, g, &t, sel_stop_id);
|
||||
}
|
||||
if let PaintKind::Image(img) = paint.kind.clone() {
|
||||
image_controls(app, ui, &target, &mut paint, img, &t);
|
||||
}
|
||||
if let PaintKind::Pattern(pt) = paint.kind.clone() {
|
||||
pattern_controls(app, ui, &target, &mut paint, pt);
|
||||
}
|
||||
if let Some(color) = edit_color {
|
||||
let mut hsb = ui
|
||||
.data(|d| d.get_temp::<(f32, f32, f32, [u8; 4])>(Id::unique("cp_hsb")))
|
||||
.filter(|(_, _, _, rgba)| *rgba == color.to_rgba8())
|
||||
.map(|(h, s, b, _)| Hsb { h, s, b })
|
||||
.unwrap_or_else(|| Hsb::from_color(color));
|
||||
let (sq, _) = ui.allocate_exact_size(vec2(240.0, 240.0), Sense::hover());
|
||||
let (c1, d1) = sv_square(ui, sq, &mut hsb);
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(240.0, 28.0), Sense::hover());
|
||||
let hr = Rect::from_min_max(pos2(hr.min.x + 48.0, hr.min.y + 8.0), pos2(hr.max.x - 16.0, hr.max.y - 8.0));
|
||||
if icons::button(
|
||||
&mut ui
|
||||
.new_child(egui::UiBuilder::new().max_rect(Rect::from_min_size(pos2(hr.min.x - 36.0, hr.center().y - 12.0), vec2(24.0, 24.0)))),
|
||||
Icon::Eyedropper,
|
||||
24.0,
|
||||
false,
|
||||
tr("Pick color (I)"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
app.run("edit.pickColor", json!({"apply": false}));
|
||||
return;
|
||||
}
|
||||
// Header.
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(240.0, 40.0), Sense::hover());
|
||||
ui.painter().text(pos2(hr.min.x + 16.0, hr.center().y), Align2::LEFT_CENTER, tr("Custom"), semibold(11.0), t.text);
|
||||
let cr = Rect::from_center_size(pos2(hr.max.x - 20.0, hr.center().y), vec2(24.0, 24.0));
|
||||
let cresp = ui.interact(cr, Id::unique("cp_close"), Sense::click());
|
||||
icons::paint(ui.painter(), cr.shrink(6.0), Icon::Close, t.icon);
|
||||
if cresp.clicked() {
|
||||
close_now = true;
|
||||
let (c2, d2) = hue_slider(ui, hr, &mut hsb);
|
||||
let (ar, _) = ui.allocate_exact_size(vec2(240.0, 20.0), Sense::hover());
|
||||
let ar = Rect::from_min_max(pos2(ar.min.x + 48.0, ar.min.y + 2.0), pos2(ar.max.x - 16.0, ar.max.y - 6.0));
|
||||
let mut new_alpha = None;
|
||||
if let Some((a, d)) = alpha_slider(ui, ar, color) {
|
||||
new_alpha = Some((a, d));
|
||||
}
|
||||
// Paint type.
|
||||
if !matches!(
|
||||
target,
|
||||
PickerTarget::Effect { .. } | PickerTarget::PageBackground | PickerTarget::Variable { .. } | PickerTarget::PluginField { .. }
|
||||
) {
|
||||
paint_type_row(ui, app, &target, &mut paint);
|
||||
let mut nc = hsb.to_color().with_alpha(color.a);
|
||||
let mut done = d1 || d2;
|
||||
let mut changed = c1 || c2;
|
||||
if let Some((a, d)) = new_alpha {
|
||||
nc = nc.with_alpha(a);
|
||||
changed = true;
|
||||
done |= d;
|
||||
}
|
||||
// The paint's own blend mode, with the paints and layers under it.
|
||||
if !matches!(
|
||||
target,
|
||||
PickerTarget::Effect { .. } | PickerTarget::PageBackground | PickerTarget::Variable { .. } | PickerTarget::PluginField { .. }
|
||||
) {
|
||||
blend_row(ui, app, &target, &mut paint);
|
||||
// Colour model fields (Hex, RGB, HSL, HSB, LCH, OKLCH or CSS) + alpha.
|
||||
if let Some((c, d)) = model_fields(ui, nc) {
|
||||
nc = c;
|
||||
hsb = Hsb::from_color(nc);
|
||||
changed = true;
|
||||
done |= d;
|
||||
}
|
||||
// Gradient stops.
|
||||
let mut edit_color: Option<Color> = match &paint.kind {
|
||||
PaintKind::Solid { color } => Some(color.with_alpha(color.a * paint.opacity)),
|
||||
_ => None,
|
||||
};
|
||||
let sel_stop_id = stop_id();
|
||||
if let PaintKind::Gradient(g) = paint.kind.clone() {
|
||||
edit_color = gradient_controls(app, ui, &target, &mut paint, g, &t, sel_stop_id);
|
||||
ui.data_mut(|d| d.insert_temp(Id::unique("cp_hsb"), (hsb.h, hsb.s, hsb.b, nc.to_rgba8())));
|
||||
// A Display P3 colour sRGB can't show: the canvas (sRGB) shows the nearest one.
|
||||
if app.session.doc().is_ok_and(|d| d.color_profile.conversion_to(figcraft_color::ColorProfile::Srgb).clips(nc)) {
|
||||
gamut_warning(ui, &t);
|
||||
}
|
||||
if let PaintKind::Image(img) = paint.kind.clone() {
|
||||
image_controls(app, ui, &target, &mut paint, img, &t);
|
||||
}
|
||||
if let PaintKind::Pattern(pt) = paint.kind.clone() {
|
||||
pattern_controls(app, ui, &target, &mut paint, pt);
|
||||
}
|
||||
if let Some(color) = edit_color {
|
||||
let mut hsb = ui
|
||||
.data(|d| d.get_temp::<(f32, f32, f32, [u8; 4])>(Id::unique("cp_hsb")))
|
||||
.filter(|(_, _, _, rgba)| *rgba == color.to_rgba8())
|
||||
.map(|(h, s, b, _)| Hsb { h, s, b })
|
||||
.unwrap_or_else(|| Hsb::from_color(color));
|
||||
let (sq, _) = ui.allocate_exact_size(vec2(240.0, 240.0), Sense::hover());
|
||||
let (c1, d1) = sv_square(ui, sq, &mut hsb);
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(240.0, 28.0), Sense::hover());
|
||||
let hr = Rect::from_min_max(pos2(hr.min.x + 48.0, hr.min.y + 8.0), pos2(hr.max.x - 16.0, hr.max.y - 8.0));
|
||||
if icons::button(
|
||||
&mut ui.new_child(
|
||||
egui::UiBuilder::new().max_rect(Rect::from_min_size(pos2(hr.min.x - 36.0, hr.center().y - 12.0), vec2(24.0, 24.0))),
|
||||
),
|
||||
Icon::Eyedropper,
|
||||
24.0,
|
||||
false,
|
||||
tr("Pick color (I)"),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
app.run("edit.pickColor", json!({"apply": false}));
|
||||
return;
|
||||
}
|
||||
let (c2, d2) = hue_slider(ui, hr, &mut hsb);
|
||||
let (ar, _) = ui.allocate_exact_size(vec2(240.0, 20.0), Sense::hover());
|
||||
let ar = Rect::from_min_max(pos2(ar.min.x + 48.0, ar.min.y + 2.0), pos2(ar.max.x - 16.0, ar.max.y - 6.0));
|
||||
let mut new_alpha = None;
|
||||
if let Some((a, d)) = alpha_slider(ui, ar, color) {
|
||||
new_alpha = Some((a, d));
|
||||
}
|
||||
let mut nc = hsb.to_color().with_alpha(color.a);
|
||||
let mut done = d1 || d2;
|
||||
let mut changed = c1 || c2;
|
||||
if let Some((a, d)) = new_alpha {
|
||||
nc = nc.with_alpha(a);
|
||||
changed = true;
|
||||
done |= d;
|
||||
}
|
||||
// Colour model fields (Hex, RGB, HSL, HSB, LCH, OKLCH or CSS) + alpha.
|
||||
if let Some((c, d)) = model_fields(ui, nc) {
|
||||
nc = c;
|
||||
hsb = Hsb::from_color(nc);
|
||||
changed = true;
|
||||
done |= d;
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(Id::unique("cp_hsb"), (hsb.h, hsb.s, hsb.b, nc.to_rgba8())));
|
||||
// A Display P3 colour sRGB can't show: the canvas (sRGB) shows the nearest one.
|
||||
if app.session.doc().is_ok_and(|d| d.color_profile.conversion_to(figcraft_color::ColorProfile::Srgb).clips(nc)) {
|
||||
gamut_warning(ui, &t);
|
||||
}
|
||||
if changed {
|
||||
match &mut paint.kind {
|
||||
PaintKind::Solid { color } => {
|
||||
*color = nc.with_alpha(1.0);
|
||||
paint.opacity = nc.a;
|
||||
}
|
||||
PaintKind::Gradient(g) => {
|
||||
let sel: usize = ui.data(|d| d.get_temp(sel_stop_id)).unwrap_or(0);
|
||||
if let Some(s) = g.stops.get_mut(sel) {
|
||||
s.color = nc;
|
||||
}
|
||||
}
|
||||
PaintKind::Image(_) | PaintKind::Pattern(_) => {}
|
||||
if changed {
|
||||
match &mut paint.kind {
|
||||
PaintKind::Solid { color } => {
|
||||
*color = nc.with_alpha(1.0);
|
||||
paint.opacity = nc.a;
|
||||
}
|
||||
write(app, &target, &paint, !done);
|
||||
}
|
||||
if done {
|
||||
finish(app);
|
||||
}
|
||||
}
|
||||
// Colours used on this page.
|
||||
let doc_colors = page_colors(app, ui);
|
||||
if !doc_colors.is_empty() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
widgets::label(ui, tr("On this page"), t.text_secondary);
|
||||
});
|
||||
// Inset on both sides: `add_space` would only indent the first row of a wrap.
|
||||
inset(ui, |ui| {
|
||||
for c in &doc_colors {
|
||||
if widgets::swatch(ui, &Paint::solid(*c), 14.0).on_hover_text(c.to_hex()).clicked() {
|
||||
set_color(app, &target, &mut paint, *c, sel_stop_id, ui);
|
||||
PaintKind::Gradient(g) => {
|
||||
let sel: usize = ui.data(|d| d.get_temp(sel_stop_id)).unwrap_or(0);
|
||||
if let Some(s) = g.stops.get_mut(sel) {
|
||||
s.color = nc;
|
||||
}
|
||||
}
|
||||
});
|
||||
PaintKind::Image(_) | PaintKind::Pattern(_) => {}
|
||||
}
|
||||
write(app, &target, &paint, !done);
|
||||
}
|
||||
// Recent colours.
|
||||
let recent = app.session.recent_colors.clone();
|
||||
if !recent.is_empty() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
widgets::label(ui, tr("Recent"), t.text_secondary);
|
||||
});
|
||||
inset(ui, |ui| {
|
||||
for c in recent.iter().take(14) {
|
||||
if widgets::swatch(ui, &Paint::solid(*c), 14.0).clicked() {
|
||||
set_color(app, &target, &mut paint, *c, sel_stop_id, ui);
|
||||
}
|
||||
if done {
|
||||
finish(app);
|
||||
}
|
||||
}
|
||||
// Colours used on this page.
|
||||
let doc_colors = page_colors(app, ui);
|
||||
if !doc_colors.is_empty() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
widgets::label(ui, tr("On this page"), t.text_secondary);
|
||||
});
|
||||
// Inset on both sides: `add_space` would only indent the first row of a wrap.
|
||||
inset(ui, |ui| {
|
||||
for c in &doc_colors {
|
||||
if widgets::swatch(ui, &Paint::solid(*c), 14.0).on_hover_text(c.to_hex()).clicked() {
|
||||
set_color(app, &target, &mut paint, *c, sel_stop_id, ui);
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
if !open || close_now || ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
finish(app);
|
||||
app.ui.picker = None;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Recent colours.
|
||||
let recent = app.session.recent_colors.clone();
|
||||
if !recent.is_empty() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(16.0);
|
||||
widgets::label(ui, tr("Recent"), t.text_secondary);
|
||||
});
|
||||
inset(ui, |ui| {
|
||||
for c in recent.iter().take(14) {
|
||||
if widgets::swatch(ui, &Paint::solid(*c), 14.0).clicked() {
|
||||
set_color(app, &target, &mut paint, *c, sel_stop_id, ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
if closed {
|
||||
close(app);
|
||||
} else if app.ui.picker.is_some() {
|
||||
// Kept only while the picker is open: a body that closed it (the eyedropper) doesn't leave
|
||||
// a stale place for the next picker to inherit.
|
||||
app.ui.picker_popover = Some(pop);
|
||||
}
|
||||
let _ = Pos2::ZERO;
|
||||
}
|
||||
|
||||
/// Escape with the picker open closes it (the selection stays); false when it was not open, so the
|
||||
/// key goes on to the binding that deselects.
|
||||
pub fn close_if_open(app: &mut FigApp) -> bool {
|
||||
if app.ui.picker.is_none() {
|
||||
return false;
|
||||
}
|
||||
close(app);
|
||||
true
|
||||
}
|
||||
|
||||
/// The colour picker closes (its X, a press outside it, Escape, or another popover took over):
|
||||
/// pending edits commit and the picker with its popover go.
|
||||
pub fn close(app: &mut FigApp) {
|
||||
finish(app);
|
||||
app.ui.picker = None;
|
||||
app.ui.picker_popover = None;
|
||||
app.ui.picker_button = None;
|
||||
}
|
||||
|
||||
/// Swatches wrapped in rows inset 16 points from both sides of the picker.
|
||||
@@ -1324,7 +1340,7 @@ mod selection_color_tests {
|
||||
s.execute("select.set", &json!({"ids":[a,b]})).unwrap();
|
||||
let mut app = FigApp::new(s, Box::new(NoServices));
|
||||
let targets = collect(app.session.doc().unwrap(), &[a, b])[0].targets.clone();
|
||||
crate::selection_colors::open_picker(&mut app, targets);
|
||||
crate::selection_colors::toggle(&mut app, targets, None);
|
||||
(app, a, b)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,13 @@ use crate::a11y::Tip;
|
||||
use crate::colorpicker::PickerTarget;
|
||||
use crate::i18n::{tr, trf};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::popover::{Hold, Open, Popover, Surface};
|
||||
use crate::theme::{Tokens, regular, semibold};
|
||||
use crate::widgets;
|
||||
|
||||
/// Content width of the popover.
|
||||
const W: f32 = 240.0;
|
||||
/// The slot's margin inside the shared popover, and the content width left of it.
|
||||
const PAD: f32 = 8.0;
|
||||
const W: f32 = 224.0;
|
||||
/// Height of a list row.
|
||||
const ROW_H: f32 = 26.0;
|
||||
/// Height of a text style's list row (its name in its own font).
|
||||
@@ -167,6 +169,9 @@ impl Picker {
|
||||
/// Open the picker for `slot` of `ids`, level with the pointer.
|
||||
pub fn open(app: &mut FigApp, ui: &Ui, ids: &[NodeId], slot: StyleSlot) {
|
||||
let y = ui.input(|i| i.pointer.interact_pos()).map_or(140.0, |p| p.y - 12.0);
|
||||
// Opening the style picker closes the colour picker (it would linger beside it).
|
||||
crate::colorpicker::close(app);
|
||||
app.ui.style_picker_popover = None;
|
||||
app.ui.style_picker = Some(Picker { slot, ids: ids.to_vec(), y, query: String::new(), edit: None });
|
||||
}
|
||||
|
||||
@@ -174,6 +179,8 @@ pub fn open(app: &mut FigApp, ui: &Ui, ids: &[NodeId], slot: StyleSlot) {
|
||||
pub fn open_for_selection(app: &mut FigApp, slot: StyleSlot, style: Option<&str>) {
|
||||
let ids = app.session.selection();
|
||||
let edit = style.and_then(|s| app.session.doc().ok()?.style(s).map(editing));
|
||||
crate::colorpicker::close(app);
|
||||
app.ui.style_picker_popover = None;
|
||||
app.ui.style_picker = Some(Picker { slot, ids, y: 140.0, query: String::new(), edit });
|
||||
}
|
||||
|
||||
@@ -516,9 +523,8 @@ fn listing(app: &FigApp, ui: &mut Ui, doc: &Document, p: &mut Picker, out: &mut
|
||||
let link = link_of(app, doc, &p.ids, slot);
|
||||
let applied = link.clone().flatten().map(|id| Key::of_local(doc, &id));
|
||||
let grid = slot.has_grid() && app.ui.style_grid;
|
||||
// Header: title, then view toggle, detach and create.
|
||||
// The title is the popover's; this row keeps the view toggle, detach and create.
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(W, 24.0), Sense::hover());
|
||||
ui.painter().text(pos2(hr.min.x + 2.0, hr.center().y), Align2::LEFT_CENTER, slot.title(), semibold(11.0), t.menu_text);
|
||||
let mut x = hr.max.x - 24.0;
|
||||
let mut slot_rect = || {
|
||||
let r = Rect::from_min_size(pos2(x, hr.min.y), vec2(24.0, 24.0));
|
||||
@@ -973,7 +979,6 @@ fn text_properties(ui: &mut Ui, doc: &Document, style: &Style, def: &TextStyle,
|
||||
/// Editing one style: name, description, its values (paints open the colour picker; text styles
|
||||
/// have their typography), update from the selection, delete.
|
||||
fn editor(ui: &mut Ui, doc: &Document, p: &mut Picker, out: &mut Frame) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let slot = p.slot;
|
||||
let has_selection = !p.ids.is_empty();
|
||||
let Some(e) = p.edit.as_mut() else { return };
|
||||
@@ -985,7 +990,6 @@ fn editor(ui: &mut Ui, doc: &Document, p: &mut Picker, out: &mut Frame) {
|
||||
if menu_icon(ui, Rect::from_min_size(hr.min, vec2(24.0, 24.0)), Icon::ChevronLeft, tr("Back to styles"), false).clicked() {
|
||||
out.act = Some(Act::Back);
|
||||
}
|
||||
ui.painter().text(pos2(hr.min.x + 30.0, hr.center().y), Align2::LEFT_CENTER, tr("Edit style"), semibold(11.0), t.menu_text);
|
||||
ui.add_space(6.0);
|
||||
caption(ui, tr("Name"));
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut e.name).font(regular(11.0)).desired_width(W));
|
||||
@@ -1074,9 +1078,9 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
if let Some(e) = &p.edit {
|
||||
commit_edit(app, &doc, e);
|
||||
}
|
||||
app.ui.style_picker_popover = None;
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
// Left of the Design panel; further left while the colour picker (which sits there) is open.
|
||||
let mut x = screen.max.x - app.ui.right_width - W - 32.0;
|
||||
@@ -1085,34 +1089,24 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
}
|
||||
let y = p.y.clamp(screen.min.y + 48.0, (screen.max.y - 460.0).max(screen.min.y + 48.0));
|
||||
let mut out = Frame { act: None, hover: None, budget: 4 };
|
||||
let area = egui::Area::new(Id::unique("style_picker_area")).order(egui::Order::Foreground).fixed_pos(pos2(x, y)).show(ctx, |ui| {
|
||||
egui::Frame::NONE.fill(t.menu).corner_radius(CornerRadius::same(10)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(W);
|
||||
if p.edit.is_some() {
|
||||
editor(ui, &doc, &mut p, &mut out);
|
||||
} else {
|
||||
listing(app, ui, &doc, &mut p, &mut out);
|
||||
}
|
||||
});
|
||||
});
|
||||
let rect = area.response.rect;
|
||||
ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::unique("style_picker_border"))).rect_stroke(
|
||||
rect,
|
||||
CornerRadius::same(10),
|
||||
Stroke::new(1.0, Color32::from_black_alpha(40)),
|
||||
egui::StrokeKind::Inside,
|
||||
);
|
||||
// Clicks in the colour picker editing this picker's style, or in a menu the editor opened,
|
||||
// keep it open.
|
||||
// The title and the X are the shared popover's now; the list keeps its tools row.
|
||||
let title = if p.edit.is_some() { tr("Edit style") } else { p.slot.title() };
|
||||
let mut pop = app.ui.style_picker_popover.take().unwrap_or_else(|| Popover::new("style_picker_area", title).pad(PAD).surface(Surface::Menu));
|
||||
pop.title = title.to_string();
|
||||
// Clicks in the colour picker editing this picker's style, in the variable picker binding one
|
||||
// of its fields, or in a menu the editor opened, keep it open.
|
||||
let picker_rect =
|
||||
app.ui.picker.as_ref().filter(|t| matches!(t, PickerTarget::Style { .. })).and(ctx.memory(|m| m.area_rect(crate::colorpicker::window_id())));
|
||||
// So do clicks in the variable picker binding one of its fields (Escape closes that first).
|
||||
let variables = crate::variable_ui::picker_rect(ctx);
|
||||
let in_popup = egui::Popup::is_any_open(ctx) || variables.is_some();
|
||||
let outside = ctx.input(|i| {
|
||||
i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|pos| !rect.contains(pos) && !picker_rect.is_some_and(|r| r.contains(pos)))
|
||||
}) && !in_popup;
|
||||
let mut close = outside || (ctx.input(|i| i.key_pressed(egui::Key::Escape)) && !in_popup);
|
||||
let holds: Vec<Rect> = picker_rect.into_iter().chain(variables).collect();
|
||||
let closed = pop.show(ctx, Open::At(pos2(x, y)), Hold { rects: &holds, escape: variables.is_some() || picker_rect.is_some() }, |ui| {
|
||||
if p.edit.is_some() {
|
||||
editor(ui, &doc, &mut p, &mut out);
|
||||
} else {
|
||||
listing(app, ui, &doc, &mut p, &mut out);
|
||||
}
|
||||
});
|
||||
let mut close = closed;
|
||||
let ids = p.ids.clone();
|
||||
let slot = p.slot;
|
||||
// The canvas shows the text style under the pointer on the selected text.
|
||||
@@ -1185,8 +1179,10 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
if matches!(app.ui.picker, Some(PickerTarget::Style { .. })) {
|
||||
app.ui.picker = None;
|
||||
}
|
||||
app.ui.style_picker_popover = None;
|
||||
} else {
|
||||
app.ui.style_picker = Some(p);
|
||||
app.ui.style_picker_popover = Some(pop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,6 +1190,8 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
|
||||
fn edit_local(app: &mut FigApp, ui: &Ui, slot: StyleSlot, id: &str) {
|
||||
let y = ui.input(|i| i.pointer.interact_pos()).map_or(140.0, |p| p.y - 12.0);
|
||||
let edit = app.session.doc().ok().and_then(|d| d.style(id).map(editing));
|
||||
crate::colorpicker::close(app);
|
||||
app.ui.style_picker_popover = None;
|
||||
app.ui.style_picker = Some(Picker { slot, ids: app.session.selection(), y, query: String::new(), edit });
|
||||
}
|
||||
|
||||
|
||||
@@ -323,17 +323,10 @@ fn text_variables_session(bind: bool) -> crate::layers_drag_tests::Ui {
|
||||
u
|
||||
}
|
||||
|
||||
/// The left edge of the style editor's content (its "Edit style" title starts 30 px in).
|
||||
/// The left edge of the style editor's content: the shared popover's slot, inside its margin.
|
||||
fn editor_left(u: &mut crate::layers_drag_tests::Ui) -> f32 {
|
||||
let mut left = None;
|
||||
for c in u.frame_output(vec![]).shapes {
|
||||
if let egui::Shape::Text(t) = &c.shape
|
||||
&& t.galley.text() == "Edit style"
|
||||
{
|
||||
left = Some(t.pos.x - 30.0);
|
||||
}
|
||||
}
|
||||
left.expect("the editor's title")
|
||||
u.frame_output(vec![]);
|
||||
u.app.ui.style_picker_popover.as_ref().expect("the editor's popover").slot.min.x
|
||||
}
|
||||
|
||||
/// The texts drawn in one frame, with their centers.
|
||||
|
||||
Reference in New Issue
Block a user