use super::{
Dragged, ExplorerEvent, FileExplorer, MenuItem, Renaming, SortBy, Status, View, icon_for,
names_for,
};
use crate::{COMPACT_WIDTH, icons, tr};
const ROW: f32 = 24.0;
const ROW_COMPACT: f32 = 48.0;
const TILE: f32 = 112.0;
const TILE_HEIGHT: f32 = TILE + 22.0;
const GAP: f32 = 4.0;
const SIZE_WIDTH: f32 = 80.0;
const TIME_WIDTH: f32 = 128.0;
const ICON: f32 = 16.0;
enum Act {
Click {
at: usize,
command: bool,
shift: bool,
},
Open(usize),
MenuOn(usize),
Menu {
id: String,
names: Vec<String>,
},
StartRename(String),
EndRename(End),
Move {
names: Vec<String>,
to: Vec<String>,
},
Navigate(Vec<String>),
Up,
Refresh,
Sort(SortBy),
View(View),
LoadMore,
Background,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum End {
Enter,
Escape,
Elsewhere,
}
#[derive(Clone, Copy)]
struct Layout {
columns: usize,
grid: bool,
}
impl FileExplorer {
pub fn ui(&mut self, ui: &mut egui::Ui) -> Vec<ExplorerEvent> {
let compact = ui.available_width() < COMPACT_WIDTH;
let focus = self.focus_id();
let focused = ui.ctx().memory(|m| m.has_focus(focus));
if focused {
ui.ctx().memory_mut(|m| {
m.set_focus_lock_filter(
focus,
egui::EventFilter {
horizontal_arrows: true,
vertical_arrows: true,
escape: true,
tab: false,
},
)
});
}
#[cfg(test)]
self.drawn.borrow_mut().clear();
let mut acts = Vec::new();
self.toolbar_ui(ui, compact, &mut acts);
ui.separator();
let mut renaming = self.renaming.take();
let layout = if self.entries.is_empty() {
self.empty_ui(ui, &mut acts);
None
} else if compact || self.view == View::List {
if !compact {
self.header_ui(ui, &mut acts);
}
Some(self.entries_ui(ui, compact, false, focused, &mut renaming, &mut acts))
} else {
Some(self.entries_ui(ui, compact, true, focused, &mut renaming, &mut acts))
};
self.renaming = renaming;
self.drag_preview_ui(ui);
let mut changed = !acts.is_empty();
if focused && let Some(layout) = layout {
changed |= self.keys(ui, layout);
}
for act in acts {
self.act(ui.ctx(), act);
}
if changed {
ui.ctx().request_repaint();
}
std::mem::take(&mut self.events)
}
fn focus_id(&self) -> egui::Id {
self.id.with("focus")
}
fn act(&mut self, ctx: &egui::Context, act: Act) {
let focus_id = self.focus_id();
let focus = |ctx: &egui::Context| ctx.memory_mut(|m| m.request_focus(focus_id));
match act {
Act::Click { at, command, shift } if at < self.order.len() => {
let shown = self.shown_names();
self.selection.click(&shown, at, command, shift);
focus(ctx);
}
Act::Open(at) if at < self.order.len() => {
focus(ctx);
self.open(at);
}
Act::MenuOn(at) if at < self.order.len() => {
let name = self.shown(at).name.clone();
if !self.selection.contains(&name) {
self.selection.only(name);
}
}
Act::Click { .. } | Act::Open(_) | Act::MenuOn(_) => {}
Act::Menu { id, names } => self.events.push(ExplorerEvent::Menu { id, names }),
Act::StartRename(name) => self.start_rename(name),
Act::EndRename(end) => {
match end {
End::Escape => self.renaming = None,
End::Enter => self.finish_rename(true),
End::Elsewhere => self.finish_rename(false),
}
if self.renaming.is_none() && end != End::Elsewhere {
focus(ctx);
}
}
Act::Move { names, to } => self.events.push(ExplorerEvent::Move { names, to }),
Act::Navigate(location) => self.navigate(location),
Act::Up => self.up(),
Act::Refresh => self.refresh(),
Act::Sort(by) => {
let mut sort = self.sort;
if sort.by == by {
sort.ascending = !sort.ascending;
} else {
sort.by = by;
sort.ascending = true;
}
self.set_sort(sort);
}
Act::View(view) => self.view = view,
Act::LoadMore => {
self.status = Status::Loading;
self.events.push(ExplorerEvent::LoadMore);
}
Act::Background => {
self.selection.none();
focus(ctx);
}
}
}
fn toolbar_ui(&self, ui: &mut egui::Ui, compact: bool, acts: &mut Vec<Act>) {
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if !compact {
for (view, icon, label) in [
(View::Grid, icons::layout_grid(), "corundum-explorer-grid"),
(View::List, icons::list(), "corundum-explorer-list"),
] {
let button = icons::button(icon, ICON).selected(self.view == view);
if ui.add(button).on_hover_text(tr!(ui, label)).clicked() {
acts.push(Act::View(view));
}
}
}
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
let up = ui
.add_enabled(
!self.location.is_empty(),
icons::button(icons::arrow_up(), ICON),
)
.on_hover_text(tr!(ui, "corundum-explorer-up"));
if up.clicked() {
acts.push(Act::Up);
}
if let Some((_, above)) = self.location.split_last() {
self.drop_target(ui, &up, above.to_vec(), None, acts);
}
let refresh = ui
.add(icons::button(icons::refresh_cw(), ICON))
.on_hover_text(tr!(ui, "corundum-explorer-refresh"));
if refresh.clicked() {
acts.push(Act::Refresh);
}
self.trail_ui(ui, acts);
});
});
});
}
fn trail_ui(&self, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
egui::ScrollArea::horizontal()
.id_salt(self.id.with("trail"))
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.stick_to_right(true)
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
let names = std::iter::once(&self.root).chain(&self.location);
let last = self.location.len();
for (depth, name) in names.enumerate() {
if depth > 0 {
ui.add(icons::image_as(
ui,
icons::chevron_right(),
ICON,
crate::theme::Tone::Muted,
crate::theme::Swatch::Neutral,
));
}
let text = if depth == last {
egui::RichText::new(name).strong()
} else {
egui::RichText::new(name)
};
let crumb = ui.add(egui::Button::new(text).frame(false));
if depth < last {
let location = self.location[..depth].to_vec();
if crumb.clicked() {
acts.push(Act::Navigate(location.clone()));
}
self.drop_target(ui, &crumb, location, None, acts);
}
}
});
});
}
fn drop_target(
&self,
ui: &egui::Ui,
response: &egui::Response,
to: Vec<String>,
folder: Option<&str>,
acts: &mut Vec<Act>,
) {
if !self.movable {
return;
}
let ours = |d: &Dragged| {
d.explorer == self.id
&& d.from == self.location
&& folder.is_none_or(|f| !d.names.iter().any(|n| n == f))
};
if response
.dnd_hover_payload::<Dragged>()
.is_some_and(|d| ours(&d))
{
ui.painter().rect_stroke(
response.rect,
4.0,
egui::Stroke::new(2.0, ui.visuals().selection.stroke.color),
egui::StrokeKind::Inside,
);
}
if let Some(dragged) = response.dnd_release_payload::<Dragged>()
&& ours(&dragged)
{
acts.push(Act::Move {
names: dragged.names.clone(),
to,
});
}
}
fn header_ui(&self, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let (rect, _) =
ui.allocate_exact_size(egui::vec2(ui.available_width(), ROW), egui::Sense::hover());
let [_, name, size, time] = columns(rect);
for (by, cell, label, right) in [
(SortBy::Name, name, "corundum-explorer-name", false),
(SortBy::Size, size, "corundum-explorer-size", true),
(SortBy::Modified, time, "corundum-explorer-modified", false),
] {
let response =
ui.interact(cell, self.id.with(("heading", label)), egui::Sense::click());
if response.clicked() {
acts.push(Act::Sort(by));
}
let colour = if response.hovered() {
ui.visuals().strong_text_color()
} else {
ui.visuals().weak_text_color()
};
let galley = galley(ui, tr!(ui, label), cell.width() - ICON, colour);
let x = if right {
cell.right() - galley.size().x
} else {
cell.left()
};
let text_pos = egui::pos2(x, cell.center().y - galley.size().y / 2.0);
let text_width = galley.size().x;
ui.painter().galley(text_pos, galley, colour);
if self.sort.by == by {
let icon = if self.sort.ascending {
icons::chevron_up()
} else {
icons::chevron_down()
};
let x = if right {
x - ICON
} else {
x + text_width + 2.0
};
let at = egui::Rect::from_min_size(
egui::pos2(x, cell.center().y - ICON / 2.0),
egui::vec2(ICON, ICON),
);
icons::paint(ui, at, icon, colour);
}
}
}
fn empty_ui(&self, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
ui.add_space(24.0);
ui.vertical_centered(|ui| match &self.status {
Status::Loading => {
ui.add(egui::Spinner::new());
ui.weak(tr!(ui, "corundum-explorer-loading"));
}
Status::Failed(error) => {
ui.colored_label(
ui.visuals().error_fg_color,
tr!(ui, "corundum-explorer-failed", error = error.as_str()),
);
if ui.button(tr!(ui, "corundum-explorer-retry")).clicked() {
acts.push(Act::Refresh);
}
}
Status::Ready => {
ui.weak(tr!(ui, "corundum-explorer-empty"));
}
});
}
fn entries_ui(
&mut self,
ui: &mut egui::Ui,
compact: bool,
grid: bool,
focused: bool,
renaming: &mut Option<Renaming>,
acts: &mut Vec<Act>,
) -> Layout {
let count = self.order.len();
let footer = self.more || self.status != Status::Ready;
let reveal = std::mem::take(&mut self.reveal);
let mut layout = Layout { columns: 1, grid };
egui::ScrollArea::vertical()
.id_salt(self.id.with("entries"))
.auto_shrink(false)
.show_viewport(ui, |ui, viewport| {
let width = ui.available_width();
let (columns, cell, pitch) = if grid {
let columns = (((width + GAP) / (TILE + GAP)).floor() as usize).max(1);
(columns, egui::vec2(TILE, TILE_HEIGHT), TILE_HEIGHT + GAP)
} else {
let height = if compact { ROW_COMPACT } else { ROW };
(1, egui::vec2(width, height), height + GAP)
};
layout.columns = columns;
let rows = count.div_ceil(columns) + usize::from(footer);
ui.set_height((pitch * rows as f32 - GAP).max(0.0));
let top = ui.max_rect().min;
let at = |n: usize| {
let (row, column) = (n / columns, n % columns);
egui::Rect::from_min_size(
top + egui::vec2(column as f32 * (TILE + GAP), row as f32 * pitch),
cell,
)
};
let background = ui.interact(
ui.max_rect().union(viewport.translate(top.to_vec2())),
self.focus_id(),
egui::Sense::click(),
);
if background.clicked() {
acts.push(Act::Background);
}
let first = (viewport.min.y / pitch).floor().max(0.0) as usize;
let last = ((viewport.max.y / pitch).ceil() as usize + 1).min(rows);
let cursor = self.selection.cursor_at(&self.shown_names());
for row in first..last {
for n in row * columns..((row + 1) * columns).min(count) {
let rect = at(n);
let response = self.entry_response(ui, rect, n, compact, acts);
let is_cursor = focused && cursor == Some(n);
if grid {
self.tile_ui(ui, rect, n, &response, is_cursor, renaming, acts);
} else {
self.row_ui(ui, rect, n, compact, &response, is_cursor, renaming, acts);
}
}
}
if footer && last == rows {
let rect = egui::Rect::from_min_size(
top + egui::vec2(0.0, (rows - 1) as f32 * pitch),
egui::vec2(width, ROW),
);
if self.status == Status::Loading {
ui.put(rect, egui::Spinner::new());
} else if self.more {
acts.push(Act::LoadMore);
}
}
if reveal && let Some(n) = cursor {
ui.scroll_to_rect(at(n), None);
}
});
layout
}
fn entry_response(
&self,
ui: &mut egui::Ui,
rect: egui::Rect,
n: usize,
compact: bool,
acts: &mut Vec<Act>,
) -> egui::Response {
let entry = self.shown(n);
let drags = self.movable && !compact;
let sense = if drags {
egui::Sense::click_and_drag()
} else {
egui::Sense::click()
};
let response = ui.interact(rect, self.id.with(("entry", &entry.name)), sense);
#[cfg(test)]
self.drawn.borrow_mut().push((entry.name.clone(), rect));
if response.clicked() {
if compact {
acts.push(Act::Open(n));
} else {
let modifiers = ui.input(|i| i.modifiers);
acts.push(Act::Click {
at: n,
command: modifiers.command,
shift: modifiers.shift,
});
}
}
if response.double_clicked() && !compact {
acts.push(Act::Open(n));
}
if response.secondary_clicked() {
acts.push(Act::MenuOn(n));
}
if drags {
if response.drag_started() {
let names = names_for(&self.selection.names, &self.shown_names(), &entry.name);
if !self.selection.contains(&entry.name) {
acts.push(Act::Click {
at: n,
command: false,
shift: false,
});
}
response.dnd_set_drag_payload(Dragged {
explorer: self.id,
from: self.location.clone(),
names,
});
}
if entry.is_folder() {
let mut to = self.location.clone();
to.push(entry.name.clone());
self.drop_target(ui, &response, to, Some(&entry.name), acts);
}
}
response.context_menu(|ui| self.menu_ui(ui, n, acts));
response
}
fn menu_ui(&self, ui: &mut egui::Ui, n: usize, acts: &mut Vec<Act>) {
let name = &self.shown(n).name;
let names = names_for(&self.selection.names, &self.shown_names(), name);
if names.len() == 1 {
if ui.button(tr!(ui, "corundum-explorer-open")).clicked() {
acts.push(Act::Open(n));
}
if self.renamable
&& ui
.add(egui::Button::image_and_text(
icons::image(ui, icons::pencil_line(), ICON),
tr!(ui, "corundum-explorer-rename"),
))
.clicked()
{
acts.push(Act::StartRename(name.clone()));
}
}
let Some(menu) = &self.menu else {
return;
};
let entries: Vec<_> = self
.entries
.iter()
.filter(|e| names.contains(&e.name))
.collect();
let items = menu(ui.ctx(), &entries);
if items.is_empty() {
return;
}
if names.len() == 1 {
ui.separator();
}
for MenuItem { id, label, icon } in items {
let clicked = match icon {
Some(icon) => ui
.add(egui::Button::image_and_text(
icons::image(ui, icon, ICON),
label,
))
.clicked(),
None => ui.button(label).clicked(),
};
if clicked {
acts.push(Act::Menu {
id,
names: names.clone(),
});
}
}
}
#[allow(clippy::too_many_arguments)]
fn row_ui(
&self,
ui: &mut egui::Ui,
rect: egui::Rect,
n: usize,
compact: bool,
response: &egui::Response,
is_cursor: bool,
renaming: &mut Option<Renaming>,
acts: &mut Vec<Act>,
) {
let entry = self.shown(n);
let colour = self.paint_backdrop(ui, rect, &entry.name, response, is_cursor);
let [icon, name, size, time] = columns(rect);
let icon_size = if compact { 28.0 } else { ICON };
let icon_cell = icon;
let icon =
egui::Rect::from_center_size(icon_cell.center(), egui::vec2(icon_size, icon_size));
let src = entry.icon.clone().unwrap_or_else(|| icon_for(entry));
icons::paint(ui, icon, src, colour);
let size_text = entry
.size
.filter(|_| !entry.is_folder())
.map(super::format_size);
let time_text = entry.modified.map(|t| (self.time)(t));
let (name_cell, detail) = if compact {
let x = name.left()..=rect.right() - 4.0;
let name = egui::Rect::from_x_y_ranges(x.clone(), rect.top() + 4.0..=rect.center().y);
let detail = egui::Rect::from_x_y_ranges(x, rect.center().y..=rect.bottom() - 4.0);
(name, Some(detail))
} else {
(name, None)
};
if !self.rename_ui(ui, name_cell, &entry.name, renaming, acts) {
paint_text(ui, name_cell, &entry.name, colour, egui::Align::Min);
}
let weak = ui.visuals().weak_text_color();
match detail {
Some(detail) => {
let text: Vec<String> = [size_text, time_text].into_iter().flatten().collect();
paint_text(ui, detail, &text.join(" · "), weak, egui::Align::Min);
}
None => {
if let Some(text) = size_text {
paint_text(ui, size, &text, weak, egui::Align::Max);
}
if let Some(text) = time_text {
paint_text(ui, time, &text, weak, egui::Align::Min);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn tile_ui(
&self,
ui: &mut egui::Ui,
rect: egui::Rect,
n: usize,
response: &egui::Response,
is_cursor: bool,
renaming: &mut Option<Renaming>,
acts: &mut Vec<Act>,
) {
let entry = self.shown(n);
let colour = self.paint_backdrop(ui, rect, &entry.name, response, is_cursor);
let picture = egui::Rect::from_min_size(rect.min, egui::vec2(TILE, TILE)).shrink(8.0);
let drawn = entry
.thumbnail
.clone()
.is_some_and(|src| paint_thumbnail(ui, picture, src));
if !drawn {
let src = entry.icon.clone().unwrap_or_else(|| icon_for(entry));
let icon = egui::Rect::from_center_size(picture.center(), egui::vec2(48.0, 48.0));
icons::paint(ui, icon, src, colour);
}
let label = egui::Rect::from_min_max(
egui::pos2(rect.left() + 4.0, rect.top() + TILE - 4.0),
rect.right_bottom() - egui::vec2(4.0, 2.0),
);
if !self.rename_ui(ui, label, &entry.name, renaming, acts) {
paint_text(ui, label, &entry.name, colour, egui::Align::Center);
}
}
fn paint_backdrop(
&self,
ui: &egui::Ui,
rect: egui::Rect,
name: &str,
response: &egui::Response,
is_cursor: bool,
) -> egui::Color32 {
let visuals = ui.visuals();
let picked = self.selection.contains(name);
if picked {
ui.painter()
.rect_filled(rect, 4.0, visuals.selection.bg_fill);
} else if response.hovered() {
ui.painter()
.rect_filled(rect, 4.0, visuals.widgets.hovered.weak_bg_fill);
}
if is_cursor {
ui.painter().rect_stroke(
rect,
4.0,
egui::Stroke::new(1.0, visuals.selection.stroke.color),
egui::StrokeKind::Inside,
);
}
if picked {
visuals.strong_text_color()
} else {
visuals.text_color()
}
}
fn rename_ui(
&self,
ui: &mut egui::Ui,
cell: egui::Rect,
name: &str,
renaming: &mut Option<Renaming>,
acts: &mut Vec<Act>,
) -> bool {
let Some(r) = renaming.as_mut().filter(|r| r.name == name) else {
return false;
};
let id = self.id.with("rename");
let allowed = r.text.trim() == r.name || self.name_allowed(r.text.trim());
let mut edit = egui::TextEdit::singleline(&mut r.text)
.id(id)
.desired_width(cell.width());
if !allowed {
edit = edit.text_color(ui.visuals().error_fg_color);
}
let cell = egui::Rect::from_center_size(
cell.center(),
egui::vec2(cell.width(), cell.height().max(ROW)),
);
let response = ui.put(cell, edit);
if !r.started {
r.started = true;
response.request_focus();
let stem = match r.name.rsplit_once('.') {
Some((stem, _)) if !stem.is_empty() => stem.chars().count(),
_ => r.name.chars().count(),
};
let mut state = egui::text_edit::TextEditState::load(ui.ctx(), id).unwrap_or_default();
state
.cursor
.set_char_range(Some(egui::text::CCursorRange::two(
egui::text::CCursor::new(0),
egui::text::CCursor::new(stem),
)));
state.store(ui.ctx(), id);
} else if response.lost_focus() {
let end = ui.input(|i| {
if i.key_pressed(egui::Key::Escape) {
End::Escape
} else if i.key_pressed(egui::Key::Enter) {
End::Enter
} else {
End::Elsewhere
}
});
acts.push(Act::EndRename(end));
}
true
}
fn drag_preview_ui(&self, ui: &egui::Ui) {
let Some(dragged) = egui::DragAndDrop::payload::<Dragged>(ui.ctx()) else {
return;
};
if dragged.explorer != self.id {
return;
}
let Some(pointer) = ui.ctx().pointer_interact_pos() else {
return;
};
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
let text = match dragged.names.as_slice() {
[one] => one.clone(),
many => tr!(ui, "corundum-explorer-items", count = many.len()),
};
egui::Area::new(self.id.with("dragged"))
.order(egui::Order::Tooltip)
.fixed_pos(pointer + egui::vec2(12.0, 12.0))
.interactable(false)
.show(ui.ctx(), |ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| ui.label(text));
});
}
fn keys(&mut self, ui: &egui::Ui, layout: Layout) -> bool {
if self.renaming.is_some() {
return false;
}
let presses: Vec<_> = ui.input(|i| {
i.events
.iter()
.filter_map(|e| match e {
egui::Event::Key {
key,
pressed: true,
modifiers,
..
} => Some((*key, *modifiers)),
_ => None,
})
.collect()
});
let columns = layout.columns as isize;
let mut did = false;
for (key, modifiers) in presses {
use egui::Key;
let shown = self.shown_names();
let shift = modifiers.shift;
let mut moved = true;
match key {
Key::ArrowUp if modifiers.alt => {
self.up();
moved = false;
}
Key::ArrowDown => self.selection.step(&shown, columns, shift),
Key::ArrowUp => self.selection.step(&shown, -columns, shift),
Key::ArrowRight if layout.grid => self.selection.step(&shown, 1, shift),
Key::ArrowLeft if layout.grid => self.selection.step(&shown, -1, shift),
Key::PageDown => self.selection.step(&shown, 10 * columns, shift),
Key::PageUp => self.selection.step(&shown, -10 * columns, shift),
Key::Home => self.selection.move_to(&shown, 0, shift),
Key::End => self
.selection
.move_to(&shown, shown.len().saturating_sub(1), shift),
Key::A if modifiers.command => {
self.selection.all(&shown);
moved = false;
}
Key::Escape => {
self.selection.none();
moved = false;
}
Key::Enter => {
if let Some(at) = self.selection.cursor_at(&shown) {
self.open(at);
}
moved = false;
}
Key::Backspace => {
self.up();
moved = false;
}
Key::F2 => {
if let Some(name) = self.selection.cursor.clone() {
self.start_rename(name);
}
moved = false;
}
_ => continue,
}
self.reveal |= moved;
did = true;
}
did
}
}
pub(super) fn columns(rect: egui::Rect) -> [egui::Rect; 4] {
let icon_width = if rect.height() > ROW {
rect.height()
} else {
ICON + 12.0
};
let icon = egui::Rect::from_min_size(rect.min, egui::vec2(icon_width, rect.height()));
let time = egui::Rect::from_min_max(
egui::pos2(rect.right() - TIME_WIDTH, rect.top()),
rect.right_bottom(),
);
let size = egui::Rect::from_min_max(
egui::pos2(time.left() - SIZE_WIDTH - 2.0 * GAP, rect.top()),
egui::pos2(time.left() - 2.0 * GAP, rect.bottom()),
);
let name = egui::Rect::from_min_max(
egui::pos2(icon.right(), rect.top()),
egui::pos2((size.left() - 2.0 * GAP).max(icon.right()), rect.bottom()),
);
[icon, name, size, time]
}
fn galley(
ui: &egui::Ui,
text: String,
width: f32,
colour: egui::Color32,
) -> std::sync::Arc<egui::Galley> {
egui::WidgetText::from(egui::RichText::new(text).color(colour)).into_galley(
ui,
Some(egui::TextWrapMode::Truncate),
width.max(0.0),
egui::TextStyle::Body,
)
}
fn paint_text(
ui: &egui::Ui,
cell: egui::Rect,
text: &str,
colour: egui::Color32,
align: egui::Align,
) {
let galley = galley(ui, text.to_owned(), cell.width(), colour);
let x = match align {
egui::Align::Min => cell.left(),
egui::Align::Center => cell.center().x - galley.size().x / 2.0,
egui::Align::Max => cell.right() - galley.size().x,
};
let pos = egui::pos2(x, cell.center().y - galley.size().y / 2.0);
ui.painter().galley(pos, galley, colour);
}
fn paint_thumbnail(ui: &egui::Ui, rect: egui::Rect, src: egui::ImageSource<'static>) -> bool {
let image = egui::Image::new(src)
.max_size(rect.size())
.maintain_aspect_ratio(true);
let Ok(poll) = image.load_for_size(ui.ctx(), rect.size()) else {
return false;
};
let Some(size) = poll.size() else {
return false;
};
let fitted = image.calc_size(rect.size(), Some(size));
image.paint_at(ui, egui::Rect::from_center_size(rect.center(), fitted));
true
}