alttabway 0.2.0

Alt-tab window switcher for wayland compositors
Documentation
use std::{fmt::Debug, iter};

use egui::{
    Align, Color32, Context, CursorIcon, Event, Frame, FullOutput, Image, Label, Layout, RawInput,
    Stroke, UiBuilder,
};

use crate::{
    gui_state::GuiState,
    wgpu_wrapper::{WgpuSurface, WgpuWrapper},
};

pub struct Gui {
    egui_ctx: Context,
    egui_renderer: Option<egui_wgpu::Renderer>,

    state: GuiState,
    cursor_icon: CursorIcon,
}

impl Debug for Gui {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Gui").finish()
    }
}

impl Default for Gui {
    fn default() -> Self {
        Self {
            egui_ctx: Context::default(),
            egui_renderer: None,
            state: Default::default(),
            cursor_icon: CursorIcon::Default,
        }
    }
}

impl Gui {
    pub fn new() -> Self {
        Gui::default()
    }

    pub fn add_item(&mut self, id: u32) {
        self.state.add_item(id);
    }

    pub fn update_item_title(&mut self, id: u32, new_title: String) {
        self.state.update_item_title(id, new_title);
    }
    pub fn update_item_app_id(&mut self, id: u32, new_app_id: String) {
        self.state.update_item_app_id(id, new_app_id);
    }
    pub fn signal_item_activation(&mut self, id: u32) {
        self.state.signal_item_activation(id);
    }
    pub fn remove_item(&mut self, id: u32) {
        self.state.remove_item(id);
    }
    pub fn get_first_item_id(&self) -> Option<u32> {
        self.state.get_first_item_id()
    }
    pub fn update_item_preview(&mut self, id: u32, preview_rgba: &[u8], preview_width: u32) {
        self.state.update_item_preview(
            id,
            (preview_rgba, preview_width as usize),
            |name, color_image| {
                self.egui_ctx
                    .load_texture(name, color_image, Default::default())
            },
        );
    }

    pub fn reset_selected_item(&mut self) {
        self.state.reset_selected_item();
    }

    pub fn get_selected_item_id(&self) -> Option<u32> {
        self.state.get_selected_item_id()
    }

    pub fn calculate_preview_size(&self, current_size: (u32, u32)) -> (u32, u32) {
        self.state.calculate_preview_size(current_size)
    }

    pub fn select_previous_item(&mut self) {
        self.state.select_previous_item()
    }

    pub fn select_next_item(&mut self) {
        self.state.select_next_item()
    }

    pub fn handle_events(&mut self, mut events: Vec<Event>) {
        for event in &mut events {
            if let Event::Key {
                key: egui::Key::Tab,
                pressed: true,
                modifiers,
                ..
            } = event
            {
                match modifiers.shift {
                    true => self.state.select_previous_item(),
                    false => self.state.select_next_item(),
                }
            }
        }

        let raw_input = RawInput {
            events,
            focused: true,
            ..Default::default()
        };
        self.build_ui(raw_input);
    }

    pub fn get_window_dimensions(&mut self) -> (u32, u32) {
        let layout = self.state.calculate_layout();
        (layout.computed.window_width, layout.computed.window_height)
    }

    fn build_ui(&mut self, raw_input: RawInput) -> FullOutput {
        let layout = self.state.calculate_layout();
        let mut hovered_item_updated = None;

        let full_output = self.egui_ctx.run(raw_input, |ctx: &Context| {
            let panel_frame = egui::Frame::new()
                .fill(egui::Color32::from_rgba_unmultiplied(25, 25, 25, 230))
                .corner_radius(layout.params.window_corner_radius);

            egui::CentralPanel::default()
                .frame(panel_frame)
                .show(ctx, |ui| {
                    for (index, (rect, item)) in layout
                        .computed
                        .item_rects
                        .iter()
                        .zip(layout.items)
                        .enumerate()
                    {
                        let mut frame_ui = ui.new_child(UiBuilder::new().max_rect(*rect));

                        let mut frame = Frame::default()
                            .stroke(Stroke::new(
                                layout.params.item_stroke as f32,
                                Color32::TRANSPARENT,
                            ))
                            .inner_margin(layout.params.item_padding as f32)
                            .corner_radius(layout.params.item_corner_radius)
                            .begin(&mut frame_ui);
                        {
                            let ui = &mut frame.content_ui;
                            ui.allocate_ui_with_layout(
                                (ui.available_width(), layout.params.title_height as f32).into(),
                                Layout::left_to_right(Align::Center),
                                |ui| ui.add(Label::new(item.get_title()).truncate()),
                            );
                            if let Some((handle, [width, height])) = item.get_preview() {
                                ui.add(
                                    Image::from_texture((
                                        handle.id(),
                                        (*width as f32, *height as f32).into(),
                                    ))
                                    .corner_radius(layout.params.preview_corner_radius),
                                );
                            } else {
                                ui.allocate_space(ui.available_size());
                            }
                        }

                        let response = frame.allocate_space(&mut frame_ui);
                        if response.hovered() {
                            hovered_item_updated = index.into();
                        }

                        if layout.selected_item == index {
                            frame.frame.stroke.color = Color32::WHITE;
                            frame.frame.fill = layout.params.item_active_background;
                        } else if let Some(hovered_item) = layout.hovered_item
                            && hovered_item == index
                        {
                            frame.frame.fill = layout.params.item_hover_background;
                        }
                        frame.paint(&frame_ui);
                    }
                });
        });

        self.cursor_icon = match hovered_item_updated {
            Some(_) => CursorIcon::PointingHand,
            None => CursorIcon::Default,
        };

        self.state.set_hovered_item(hovered_item_updated);

        full_output
    }

    pub fn needs_repaint(&self) -> bool {
        self.state.needs_repaint()
    }

    pub fn get_cursor_icon(&mut self) -> &CursorIcon {
        &self.cursor_icon
    }

    pub fn paint(&mut self, wgpu: &mut WgpuWrapper, wsurf: &mut WgpuSurface) -> anyhow::Result<()> {
        let _span = tracing::trace_span!("Paint").entered();

        let output = wsurf.surface.get_current_texture()?;

        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        let width = wsurf.surface_config.width;
        let height = wsurf.surface_config.height;

        // Build egui UI with collected events
        let raw_input = egui::RawInput {
            screen_rect: Some(egui::Rect::from_min_size(
                egui::Pos2::ZERO,
                egui::vec2(width as f32, height as f32),
            )),
            focused: true,
            ..Default::default()
        };

        let full_output = self.build_ui(raw_input);

        let mut encoder = wgpu
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render Encoder"),
            });

        let screen_descriptor = egui_wgpu::ScreenDescriptor {
            size_in_pixels: [width, height],
            pixels_per_point: 1.0,
        };

        let clipped_primitives = self.egui_ctx.tessellate(full_output.shapes, 1.0);

        let egui_renderer = self.egui_renderer.get_or_insert_with(|| {
            egui_wgpu::Renderer::new(
                &wgpu.device,
                wsurf.surface_config.format,
                egui_wgpu::RendererOptions::default(),
            )
        });

        tracing::trace!("Updating textures");

        for (id, image_delta) in &full_output.textures_delta.set {
            egui_renderer.update_texture(&wgpu.device, &wgpu.queue, *id, image_delta);
        }

        tracing::trace!("Updating buffers");

        egui_renderer.update_buffers(
            &wgpu.device,
            &wgpu.queue,
            &mut encoder,
            &clipped_primitives,
            &screen_descriptor,
        );

        {
            tracing::trace!("Beginning render pass");

            let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.0,
                            g: 0.0,
                            b: 0.0,
                            a: 0.0,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });

            egui_renderer.render(
                &mut render_pass.forget_lifetime(),
                &clipped_primitives,
                &screen_descriptor,
            );
        }

        tracing::trace!("Freeing textures");
        for id in &full_output.textures_delta.free {
            egui_renderer.free_texture(id);
        }

        tracing::trace!("Submitting queue");
        wgpu.queue.submit(iter::once(encoder.finish()));

        tracing::trace!("Presenting output");
        output.present();

        tracing::trace!("Completed");
        self.state.mark_repainted();

        Ok(())
    }
}