diff --git a/crates/cp-panel/src/face.rs b/crates/cp-panel/src/face.rs index 60a2e737..1329cdab 100644 --- a/crates/cp-panel/src/face.rs +++ b/crates/cp-panel/src/face.rs @@ -30,6 +30,7 @@ const KEYS_PER_LINE: usize = 56; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Face { Found, + FoundThumb, Thumb, Paint, Block(i32), @@ -52,7 +53,11 @@ pub struct Seen<'a> { impl Face { pub fn of(seen: &Seen<'_>) -> Self { if seen.found { - return Self::Found; + return if seen.thumb { + Self::FoundThumb + } else { + Self::Found + }; } if seen.thumb { return Self::Thumb; @@ -79,6 +84,7 @@ impl Face { pub fn as_str(self) -> &'static str { match self { Self::Found => "found", + Self::FoundThumb => "found-thumb", Self::Thumb => "thumb", Self::Paint => "paint", Self::Block(_) => "block", @@ -100,7 +106,7 @@ impl Face { pub fn body_px(self) -> f32 { match self { Self::Found | Self::Link => LINE, - Self::Thumb => THUMB, + Self::Thumb | Self::FoundThumb => THUMB, Self::Paint => PAINT, Self::Block(lines) => BLOCK_PAD + MONO_LINE * lines as f32, Self::Keys(lines) => KEYS_LINE * lines as f32, @@ -110,7 +116,10 @@ impl Face { } pub fn shut_px(self) -> f32 { - TOP + META + GAP + self.body_px() + BOTTOM + BETWEEN + match self { + Self::Paint => TOP + META + BOTTOM + BETWEEN, + _ => TOP + META + GAP + self.body_px() + BOTTOM + BETWEEN, + } } } @@ -200,7 +209,7 @@ pub fn open_body_px(face: Face, opened: &Opened, claims: i32, room: f32) -> f32 GAP + NOTE }; match face { - Face::Thumb => OPEN_THUMB + GAP + NOTE, + Face::Thumb | Face::FoundThumb => OPEN_THUMB + GAP + NOTE, Face::Words(_) | Face::Found => LINE * opened.lines as f32 + note, Face::Block(_) | Face::Keys(_) | Face::Link if claims > 0 => { BLOCK_PAD + MONO_LINE * claims as f32 diff --git a/crates/cp-panel/src/face_test.rs b/crates/cp-panel/src/face_test.rs index b7037f91..164b8ce3 100644 --- a/crates/cp-panel/src/face_test.rs +++ b/crates/cp-panel/src/face_test.rs @@ -16,6 +16,13 @@ fn a_search_hit_a_picture_and_a_colour_win_over_the_kind() { let mut one = seen(Some(Kind::Code), "fn main() {}"); one.found = true; assert_eq!(Face::of(&one), Face::Found); + one.thumb = true; + assert_eq!( + Face::of(&one), + Face::FoundThumb, + "a picture found by its words keeps its thumbnail" + ); + one.found = false; one.found = false; one.thumb = true; assert_eq!(Face::of(&one), Face::Thumb); @@ -166,3 +173,10 @@ fn an_open_card_is_as_tall_as_its_body_its_note_and_its_keys() { TOP + META + GAP + GAP + KEYS_ROW + BOTTOM + BETWEEN ); } + +#[test] +fn a_colour_sits_on_a_single_row_while_closed() { + assert_eq!(Face::Paint.shut_px(), TOP + META + BOTTOM + BETWEEN); + assert_eq!(Face::FoundThumb.body_px(), THUMB); + assert_eq!(Face::FoundThumb.as_str(), "found-thumb"); +} diff --git a/crates/cp-panel/src/view.rs b/crates/cp-panel/src/view.rs index b3f1bb33..93c417b3 100644 --- a/crates/cp-panel/src/view.rs +++ b/crates/cp-panel/src/view.rs @@ -246,6 +246,7 @@ pub fn card_of(row: &Listed, now: i64, meta: Option<&MetaOfOne>) -> Card { || crate::opening::can_open_link(row.kind, &row.preview), can_drag: crate::dragging::can_drag(row.kind, &paths_of(row)), paint: shown_as_colour(row).unwrap_or_default(), + paint_said: paint_said(row).into(), paints: shown_as_colour(row).is_some(), thumb: slint::Image::default(), has_thumb: row.thumb_path.is_some(), @@ -448,6 +449,27 @@ pub fn shown_as_colour(row: &Listed) -> Option { )) } +pub fn paint_said(row: &Listed) -> String { + let Some(said) = row + .kind + .filter(|kind| *kind == Kind::Color) + .and_then(|_| cp_core::paint::rgba_of(row.preview.lines().next()?)) + else { + return String::new(); + }; + if said.alpha == 255 { + format!("rgb({}, {}, {})", said.red, said.green, said.blue) + } else { + format!( + "rgba({}, {}, {}, {:.2})", + said.red, + said.green, + said.blue, + f32::from(said.alpha) / 255.0 + ) + } +} + pub fn squeezed_of(text: &str) -> String { let mut out = String::with_capacity(text.len()); let mut spaced = true; diff --git a/crates/cp-panel/src/view_test.rs b/crates/cp-panel/src/view_test.rs index 49740ba3..b1ac97ad 100644 --- a/crates/cp-panel/src/view_test.rs +++ b/crates/cp-panel/src/view_test.rs @@ -920,3 +920,13 @@ fn a_hit_inside_a_text_of_many_lines_is_shown_on_one() { assert_eq!(tail, " gesto que en o… "); assert!(![lead, hit, tail].concat().contains('\n')); } + +#[test] +fn a_colour_says_its_channels_and_anything_else_says_nothing() { + let mut colour = row(Some(Kind::Color)); + colour.preview = "#FF8800".into(); + assert_eq!(paint_said(&colour), "rgb(255, 136, 0)"); + colour.preview = "#FF880080".into(); + assert_eq!(paint_said(&colour), "rgba(255, 136, 0, 0.50)"); + assert_eq!(paint_said(&row(Some(Kind::Text))), ""); +} diff --git a/crates/cp-panel/ui/cards.slint b/crates/cp-panel/ui/cards.slint index efc241e8..7e60b955 100644 --- a/crates/cp-panel/ui/cards.slint +++ b/crates/cp-panel/ui/cards.slint @@ -22,13 +22,14 @@ export component Action inherits Rectangle { in property icon; in property hot: Theme.body; in property awake: true; + in property marked; callback pressed(); width: 22px; height: 22px; border-radius: 6px; - background: touch.has-hover ? #333853 : transparent; + background: touch.has-hover ? #333853 : root.marked ? Theme.tile : transparent; animate background { duration: 100ms; } - Icon { source: root.icon; width: 14px; height: 14px; tint: touch.has-hover ? root.hot : Theme.muted; } + Icon { source: root.icon; width: 14px; height: 14px; tint: touch.has-hover ? root.hot : root.marked ? Theme.accent-dim : Theme.muted; } touch := TouchArea { enabled: root.awake; clicked => { root.pressed(); } @@ -247,12 +248,20 @@ component KeyChip inherits Rectangle { in property keys; in property says; in property on; + callback pressed(); + accessible-role: AccessibleRole.button; + accessible-label: root.says; height: 22px; width: chip-row.preferred-width; border-radius: 6px; - background: on ? Theme.selected : Theme.field; + background: touch.has-hover ? Theme.row-hover : on ? Theme.selected : Theme.field; + animate background { duration: 100ms; } border-width: 1px; - border-color: on ? Theme.selected-edge : Theme.field-border; + border-color: on || touch.has-hover ? Theme.selected-edge : Theme.field-border; + touch := TouchArea { + mouse-cursor: MouseCursor.pointer; + clicked => { root.pressed(); } + } chip-row := HorizontalLayout { padding-left: 8px; padding-right: 8px; @@ -282,6 +291,8 @@ export component CardView inherits Rectangle { callback copy(int); callback open-asked(int); callback forms(int); + callback paste(int); + callback plain(int); callback edit(int); callback named(int, string); callback naming-dropped(); @@ -360,7 +371,25 @@ export component CardView inherits Rectangle { max-width: 180px; vertical-alignment: center; } - if !one-kind : Text { + if card.face == "paint" : Rectangle { + width: 18px; + height: 18px; + y: (parent.height - 18px) / 2; + border-radius: 5px; + background: card.paint; + border-width: 1px; + border-color: Theme.selected-edge; + } + if card.face == "paint" : Text { + text: card.squeezed; + color: Theme.body; + font-size: 13px; + font-family: "Cascadia Mono"; + overflow: elide; + wrap: no-wrap; + vertical-alignment: center; + } + if !one-kind && card.face != "paint" : Text { text: card.title; color: Hue.of(card.kind); font-size: 11px; @@ -420,7 +449,7 @@ export component CardView inherits Rectangle { if card.can-open && !card.broken : Action { icon: @image-url("../assets/icons/external-link.svg"); says: Words.open-it; pressed => { root.open-asked(card.id); } } Action { icon: @image-url("../assets/icons/pencil.svg"); says: Words.name-it; pressed => { root.edit(card.id); } } if !card.broken : Action { icon: @image-url("../assets/icons/copy.svg"); says: Words.copy-it; pressed => { root.copy(card.id); } } - if !card.broken : Action { icon: @image-url("../assets/icons/arrow-right-to-line.svg"); says: Words.paste-as; pressed => { root.forms(card.id); } } + if !card.broken : Action { icon: @image-url("../assets/icons/arrow-right-to-line.svg"); says: Words.paste-as; marked: true; pressed => { root.forms(card.id); } } Action { icon: card.pinned ? @image-url("../assets/icons/pin-off.svg") : @image-url("../assets/icons/pin.svg"); says: card.pinned ? Words.unpin : Words.pin; pressed => { root.pin(card.id, !card.pinned); } } Action { icon: @image-url("../assets/icons/trash-2.svg"); says: Words.remove; hot: Theme.danger; pressed => { root.remove(card.id); } } } @@ -471,26 +500,25 @@ export component CardView inherits Rectangle { } } - if !open && card.face == "paint" : HorizontalLayout { - height: 24px; - spacing: 10px; + if !open && card.face == "found-thumb" : HorizontalLayout { + height: 50px; + spacing: 12px; Rectangle { - width: 24px; - height: 24px; - border-radius: 6px; - background: card.paint; - border-width: 1px; - border-color: Theme.selected-edge; + width: 50px; + height: 50px; + border-radius: 7px; + clip: true; + background: Theme.tile; + Image { source: card.thumb; width: parent.width; height: parent.height; image-fit: cover; } } - Text { - text: card.squeezed; - color: Theme.body; - font-size: 13px; - font-family: "Cascadia Mono"; - overflow: elide; - wrap: no-wrap; - horizontal-stretch: 1; - vertical-alignment: center; + VerticalLayout { + alignment: center; + HorizontalLayout { + height: 19px; + Text { text: card.lead; color: Theme.muted; font-size: 13.5px; vertical-alignment: center; } + Text { text: card.hit; color: Theme.accent; font-size: 13.5px; font-weight: 700; vertical-alignment: center; } + Text { text: card.tail; color: Theme.body; font-size: 13.5px; overflow: elide; horizontal-stretch: 1; vertical-alignment: center; } + } } } @@ -576,14 +604,14 @@ export component CardView inherits Rectangle { overflow: elide; } - if open && card.face == "thumb" : Rectangle { + if open && (card.face == "thumb" || card.face == "found-thumb") : Rectangle { height: 146px; border-radius: 8px; clip: true; background: Theme.tile; Image { source: card.thumb; width: parent.width; height: parent.height; image-fit: contain; } } - if open && card.face == "thumb" : Text { + if open && (card.face == "thumb" || card.face == "found-thumb") : Text { height: 15px; text: card.media-measures != "" ? card.media-measures : card.headline; color: Theme.muted; @@ -651,9 +679,9 @@ export component CardView inherits Rectangle { border-color: Theme.selected-edge; } Text { - text: card.squeezed; - color: Theme.body; - font-size: 14px; + text: card.paint-said; + color: Theme.muted; + font-size: 13px; font-family: "Cascadia Mono"; vertical-alignment: center; overflow: elide; @@ -668,9 +696,9 @@ export component CardView inherits Rectangle { if open && !card.broken : HorizontalLayout { height: 22px; spacing: 6px; - KeyChip { keys: "⏎"; says: Words.paste-it; on: true; } - KeyChip { keys: "⇧⏎"; says: Words.paste-plain; } - KeyChip { keys: Theme.apple ? "⌥⏎" : "Alt ⏎"; says: Words.paste-as-short; } + KeyChip { keys: "⏎"; says: Words.paste-it; on: true; pressed => { root.paste(card.id); } } + KeyChip { keys: "⇧⏎"; says: Words.paste-plain; pressed => { root.plain(card.id); } } + KeyChip { keys: Theme.apple ? "⌥⏎" : "Alt ⏎"; says: Words.paste-as-short; pressed => { root.forms(card.id); } } Rectangle { horizontal-stretch: 1; } } } @@ -830,7 +858,7 @@ export component CardView inherits Rectangle { if card.can-open && !card.broken : Action { icon: @image-url("../assets/icons/external-link.svg"); says: Words.open-it; awake: lit; pressed => { root.open-asked(card.id); } } Action { icon: @image-url("../assets/icons/pencil.svg"); says: Words.name-it; awake: lit; pressed => { root.edit(card.id); } } if !card.broken : Action { icon: @image-url("../assets/icons/copy.svg"); says: Words.copy-it; awake: lit; pressed => { root.copy(card.id); } } - if !card.broken : Action { icon: @image-url("../assets/icons/arrow-right-to-line.svg"); says: Words.paste-as; awake: lit; pressed => { root.forms(card.id); } } + if !card.broken : Action { icon: @image-url("../assets/icons/arrow-right-to-line.svg"); says: Words.paste-as; awake: lit; marked: true; pressed => { root.forms(card.id); } } Action { icon: card.pinned ? @image-url("../assets/icons/pin-off.svg") : @image-url("../assets/icons/pin.svg"); says: card.pinned ? Words.unpin : Words.pin; awake: lit; pressed => { root.pin(card.id, !card.pinned); } } Action { icon: @image-url("../assets/icons/trash-2.svg"); says: Words.remove; hot: Theme.danger; awake: lit; pressed => { root.remove(card.id); } } } diff --git a/crates/cp-panel/ui/panel.slint b/crates/cp-panel/ui/panel.slint index 5cbba42d..170c41cb 100644 --- a/crates/cp-panel/ui/panel.slint +++ b/crates/cp-panel/ui/panel.slint @@ -710,6 +710,8 @@ export component Panel inherits Window { } layout: root.layout; copy(id) => { root.copy(id); } + paste(id) => { root.paste(id); } + plain(id) => { root.paste-as(id, "plain-text"); } forms(id) => { root.current = index; root.opened = false; diff --git a/crates/cp-panel/ui/types.slint b/crates/cp-panel/ui/types.slint index aa5f753e..8569145b 100644 --- a/crates/cp-panel/ui/types.slint +++ b/crates/cp-panel/ui/types.slint @@ -50,6 +50,7 @@ export struct Card { open-px: length, mono: bool, paint: color, + paint-said: string, paints: bool, can-open: bool, can-drag: bool,