notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

viewer.rs (9908B)


      1 use bitflags::bitflags;
      2 use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
      3 use notedeck::media::{MediaInfo, ViewMediaInfo};
      4 use notedeck::{ImageType, Images};
      5 
      6 bitflags! {
      7     #[repr(transparent)]
      8     #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
      9     pub struct MediaViewerFlags: u64 {
     10         /// Open the media viewer fullscreen
     11         const Fullscreen = 1 << 0;
     12 
     13         /// Enable a transition animation
     14         const Transition = 1 << 1;
     15 
     16         /// Are we open or closed?
     17         const Open = 1 << 2;
     18     }
     19 }
     20 
     21 /// State used in the MediaViewer ui widget.
     22 pub struct MediaViewerState {
     23     /// When
     24     pub media_info: ViewMediaInfo,
     25     pub scene_rect: Option<Rect>,
     26     pub flags: MediaViewerFlags,
     27     pub anim_id: egui::Id,
     28 }
     29 
     30 impl Default for MediaViewerState {
     31     fn default() -> Self {
     32         Self {
     33             anim_id: egui::Id::new("notedeck-fullscreen-media-viewer"),
     34             media_info: Default::default(),
     35             scene_rect: None,
     36             flags: MediaViewerFlags::Transition | MediaViewerFlags::Fullscreen,
     37         }
     38     }
     39 }
     40 
     41 impl MediaViewerState {
     42     pub fn new(anim_id: egui::Id) -> Self {
     43         Self {
     44             anim_id,
     45             ..Default::default()
     46         }
     47     }
     48 
     49     /// How much is our media viewer open
     50     pub fn open_amount(&self, ui: &mut egui::Ui) -> f32 {
     51         ui.ctx().animate_bool_with_time_and_easing(
     52             self.anim_id,
     53             self.flags.contains(MediaViewerFlags::Open),
     54             0.3,
     55             egui::emath::easing::cubic_out,
     56         )
     57     }
     58 
     59     /// Should we show the control even if we're closed?
     60     /// Needed for transition animation
     61     pub fn should_show(&self, ui: &mut egui::Ui) -> bool {
     62         if self.flags.contains(MediaViewerFlags::Open) {
     63             return true;
     64         }
     65 
     66         // we are closing
     67         self.open_amount(ui) > 0.0
     68     }
     69 }
     70 
     71 /// A panning, scrolling, optionally fullscreen, and tiling media viewer
     72 pub struct MediaViewer<'a> {
     73     state: &'a mut MediaViewerState,
     74 }
     75 
     76 impl<'a> MediaViewer<'a> {
     77     pub fn new(state: &'a mut MediaViewerState) -> Self {
     78         Self { state }
     79     }
     80 
     81     /// Is this
     82     pub fn fullscreen(self, enable: bool) -> Self {
     83         self.state.flags.set(MediaViewerFlags::Fullscreen, enable);
     84         self
     85     }
     86 
     87     /// Enable open transition animation
     88     pub fn transition(self, enable: bool) -> Self {
     89         self.state.flags.set(MediaViewerFlags::Transition, enable);
     90         self
     91     }
     92 
     93     pub fn ui(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
     94         if self.state.flags.contains(MediaViewerFlags::Fullscreen) {
     95             egui::Window::new("Media Viewer")
     96                 .title_bar(false)
     97                 .fixed_size(ui.ctx().screen_rect().size())
     98                 .fixed_pos(ui.ctx().screen_rect().min)
     99                 .frame(egui::Frame::NONE)
    100                 .show(ui.ctx(), |ui| self.ui_content(images, ui))
    101                 .unwrap() // SAFETY: we are always open
    102                 .inner
    103                 .unwrap()
    104         } else {
    105             self.ui_content(images, ui)
    106         }
    107     }
    108 
    109     fn ui_content(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
    110         let avail_rect = ui.available_rect_before_wrap();
    111 
    112         let scene_rect = if let Some(scene_rect) = self.state.scene_rect {
    113             scene_rect
    114         } else {
    115             self.state.scene_rect = Some(avail_rect);
    116             avail_rect
    117         };
    118 
    119         let zoom_range: egui::Rangef = (0.0..=10.0).into();
    120 
    121         let is_open = self.state.flags.contains(MediaViewerFlags::Open);
    122         let can_transition = self.state.flags.contains(MediaViewerFlags::Transition);
    123         let open_amount = self.state.open_amount(ui);
    124         let transitioning = if !can_transition {
    125             false
    126         } else if is_open {
    127             open_amount < 1.0
    128         } else {
    129             open_amount > 0.0
    130         };
    131 
    132         let mut trans_rect = if transitioning {
    133             let clicked_img = &self.state.media_info.clicked_media();
    134             let src_pos = &clicked_img.original_position;
    135             let in_scene_pos = Self::first_image_rect(ui, clicked_img, images);
    136             transition_scene_rect(
    137                 &avail_rect,
    138                 &zoom_range,
    139                 &in_scene_pos,
    140                 src_pos,
    141                 open_amount,
    142             )
    143         } else {
    144             scene_rect
    145         };
    146 
    147         // Draw background
    148         ui.painter().rect_filled(
    149             avail_rect,
    150             0.0,
    151             egui::Color32::from_black_alpha((200.0 * open_amount) as u8),
    152         );
    153 
    154         let scene = egui::Scene::new().zoom_range(zoom_range);
    155 
    156         // We are opening, so lock controls
    157         /* TODO(jb55): 0.32
    158         if transitioning {
    159             scene = scene.sense(egui::Sense::hover());
    160         }
    161         */
    162 
    163         let resp = scene.show(ui, &mut trans_rect, |ui| {
    164             Self::render_image_tiles(&self.state.media_info.medias, images, ui, open_amount);
    165         });
    166 
    167         self.state.scene_rect = Some(trans_rect);
    168 
    169         resp.response
    170     }
    171 
    172     /// The rect of the first image to be placed.
    173     /// This is mainly used for the transition animation
    174     ///
    175     /// TODO(jb55): replace this with a "placed" variant once
    176     /// we have image layouts
    177     fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
    178         // fetch image texture
    179         let Some(texture) = images.latest_texture(ui, &media.url, ImageType::Content(None)) else {
    180             tracing::error!("could not get latest texture in first_image_rect");
    181             return Rect::ZERO;
    182         };
    183 
    184         // the area the next image will be put in.
    185         let mut img_rect = ui.available_rect_before_wrap();
    186 
    187         let size = texture.size_vec2();
    188         img_rect.set_height(size.y);
    189         img_rect.set_width(size.x);
    190         img_rect
    191     }
    192 
    193     ///
    194     /// Tile a scene with images.
    195     ///
    196     /// TODO(jb55): Let's improve image tiling over time, spiraling outward. We
    197     /// should have a way to click "next" and have the scene smoothly transition and
    198     /// focus on the next image
    199     fn render_image_tiles(
    200         infos: &[MediaInfo],
    201         images: &mut Images,
    202         ui: &mut egui::Ui,
    203         open_amount: f32,
    204     ) {
    205         for info in infos {
    206             let url = &info.url;
    207 
    208             // fetch image texture
    209             let Some(texture) = images.latest_texture(ui, url, ImageType::Content(None)) else {
    210                 continue;
    211             };
    212 
    213             // the area the next image will be put in.
    214             let mut img_rect = ui.available_rect_before_wrap();
    215             /*
    216             if !ui.is_rect_visible(img_rect) {
    217                 // just stop rendering images if we're going out of the scene
    218                 // basic culling when we have lots of images
    219                 break;
    220             }
    221             */
    222 
    223             {
    224                 let size = texture.size_vec2();
    225                 img_rect.set_height(size.y);
    226                 img_rect.set_width(size.x);
    227                 let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
    228 
    229                 // image actions
    230                 //let response = ui.interact(render_rect, carousel_id.with("img"), Sense::click());
    231 
    232                 /*
    233                 if response.clicked() {
    234                 } else if background_response.clicked() {
    235                 }
    236                 */
    237 
    238                 // Paint image
    239                 ui.painter().image(
    240                     texture.id(),
    241                     img_rect,
    242                     uv,
    243                     Color32::from_white_alpha((open_amount * 255.0) as u8),
    244                 );
    245 
    246                 ui.advance_cursor_after_rect(img_rect);
    247             }
    248         }
    249     }
    250 }
    251 
    252 /// Helper: lerp a TSTransform (uniform scale + translation)
    253 fn lerp_ts(a: TSTransform, b: TSTransform, t: f32) -> TSTransform {
    254     let s = egui::lerp(a.scaling..=b.scaling, t);
    255     let p = a.translation + (b.translation - a.translation) * t;
    256     TSTransform {
    257         scaling: s,
    258         translation: p,
    259     }
    260 }
    261 
    262 /// Calculate the open/close amount and transition rect
    263 pub fn transition_scene_rect(
    264     outer_rect: &Rect,
    265     zoom_range: &Rangef,
    266     image_rect_in_scene: &Rect, // e.g. Rect::from_min_size(Pos2::ZERO, image_size)
    267     timeline_global_rect: &Rect, // saved from timeline Response.rect
    268     open_amt: f32,              // stable ID per media item
    269 ) -> Rect {
    270     // Compute the two endpoints:
    271     let from = fit_to_rect_in_scene(timeline_global_rect, image_rect_in_scene, zoom_range);
    272     let to = fit_to_rect_in_scene(outer_rect, image_rect_in_scene, zoom_range);
    273 
    274     // Interpolate transform and convert to scene_rect expected by Scene::show:
    275     let lerped = lerp_ts(from, to, open_amt);
    276 
    277     lerped.inverse() * (*outer_rect)
    278 }
    279 
    280 /// Creates a transformation that fits a given scene rectangle into the available screen size.
    281 ///
    282 /// The resulting visual scene bounds can be larger, due to letterboxing.
    283 ///
    284 /// Returns the transformation from `scene` to `global` coordinates.
    285 fn fit_to_rect_in_scene(
    286     rect_in_global: &Rect,
    287     rect_in_scene: &Rect,
    288     zoom_range: &Rangef,
    289 ) -> TSTransform {
    290     // Compute the scale factor to fit the bounding rectangle into the available screen size:
    291     let scale = rect_in_global.size() / rect_in_scene.size();
    292 
    293     // Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
    294     let scale = scale.min_elem();
    295 
    296     // Clamp scale to what is allowed
    297     let scale = zoom_range.clamp(scale);
    298 
    299     // Compute the translation to center the bounding rect in the screen:
    300     let center_in_global = rect_in_global.center().to_vec2();
    301     let center_scene = rect_in_scene.center().to_vec2();
    302 
    303     // Set the transformation to scale and then translate to center.
    304     TSTransform::from_translation(center_in_global - scale * center_scene)
    305         * TSTransform::from_scaling(scale)
    306 }