Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/tinyclient/src/gui/components/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ impl Render for Tip {
.font_family(theme::FONT)
.type_small()
.text_color(theme::text())
.child(self.words.clone())
.children(self.shortcut.clone().map(|keys| div().text_color(theme::text_faint()).child(keys)))
.child(div().flex_1().min_w_0().whitespace_normal().overflow_hidden().child(self.words.clone()))
.children(self.shortcut.clone().map(|keys| div().flex_none().whitespace_nowrap().text_color(theme::text_faint()).child(keys)))
}
}

Expand Down
17 changes: 15 additions & 2 deletions crates/tinyclient/src/gui/conversation/images.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ pub fn decode(path: &Path, edge: u32, crop: Option<f32>) -> Option<Decoded> {

#[cfg(not(target_os = "macos"))]
fn decode_portable(path: &Path, edge: u32, crop: Option<f32>) -> Option<Decoded> {
let edge = edge.clamp(1, 8192);
let mut img = image::open(path).ok()?.into_rgba8();
let mut img = image::ImageReader::open(path).ok()?.with_guessed_format().ok()?.decode().ok()?.into_rgba8();
let (w, h) = (img.width(), img.height());
// Cache paths have no suffix, and a requested thumbnail must not enlarge a small source.
let edge = edge.clamp(1, 8192).min(w.max(h));
let (nw, nh) = if w >= h {
let nh = ((h as u64 * edge as u64) / w as u64).max(1) as u32;
(edge, nh)
Expand Down Expand Up @@ -693,6 +694,18 @@ mod tests {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(name)
}

#[test]
fn cached_images_decode_without_filename_extensions() {
for (ix, format) in [image::ImageFormat::Png, image::ImageFormat::Jpeg, image::ImageFormat::WebP].into_iter().enumerate() {
let path = std::env::temp_dir().join(format!("tc-extensionless-{}-{ix}", std::process::id()));
let image = image::RgbImage::from_pixel(32, 16, image::Rgb([220, 40, 60]));
image.save_with_format(&path, format).unwrap();
let decoded = decode(&path, 16, None).expect("cache files have no extension; use their bytes");
assert_eq!((decoded.width, decoded.height), (16, 8));
std::fs::remove_file(path).unwrap();
}
}

#[test]
fn a_moving_picture_is_read_for_frames_only_when_they_are_wanted() {
// Only these are read whole: a GIF, a WebP marked animated, a PNG with an animation chunk.
Expand Down
4 changes: 3 additions & 1 deletion docs/design/polish/01-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ Items that do not apply are disabled, never hidden; names follow the open place
- **Animates, only**: menu and popover in 120 / out 80 (opacity + 4 toward anchor); modal and toast in 160 / out 100
(opacity + 8 rise); scrollbar fade 150 after 800 idle; typing dots stepped at 400. Reduce Motion: all 0. Nothing else.
- **Tooltips**: 500 delay, appear whole, 0 delay for 1.5 s after one closes, gone on mouse-down or leave. Every
icon-only button has one, with its shortcut in `text_faint`.
icon-only button has one, with its shortcut in `text_faint`. Text wraps within
the 240-point tooltip; the shortcut stays on one line beside it. Long failure
reasons must not paint outside the tooltip background.
- **Focus**, keyboard only: rows and menu items a 2 inset `focus` ring over any fill; controls a 2 gap then a 2 ring;
fields a `focus` border + 3 halo while focused. The list cursor is the ring, the open conversation the fill (`07` R8).
3 changes: 3 additions & 0 deletions docs/design/polish/06-composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ removing a file (Discord) and replacing a photo (Telegram) are P2.
are computed from the trigger bounds before layout. Grid = `uniform_list` of rows (9 emoji / 1 justified GIF row / 4
stickers), so ~12 rows exist; images decode in `Images` only for visible rows and drop on scroll-off; the entity and
`media/<net>/pick/` die on close.
- **Still-image decode**: media cache filenames have no extension. On Linux and
Windows, detect the image format from its bytes, as on macOS. Thumbnail decoding
shrinks large sources but never enlarges a smaller source to the requested edge.
- **GIF decode rule**: every cell is a first-frame still (`images.rs` reads index 0 only). The one hovered cell swaps to
`img()` of the provider's smallest GIF rendition (Tenor `tinygif`, ~220 px): this rev's `img()` steps frames itself
and stops under `reduce_motion()`. Played only if width × height × 4 × frames ≤ 8 MB, else it stays still; dropped
Expand Down
Loading