use egui::{self, NumExt, RichText, Sense, TextBuffer, TextStyle, Ui, Vec2, epaint};
#[inline]
pub fn rule(ui: &mut Ui, end_line: bool) {
ui.add(egui::Separator::default().horizontal());
if end_line {
newline(ui);
}
}
#[inline]
pub fn soft_break(ui: &mut Ui) {
ui.label(" ");
}
#[inline]
pub fn newline(ui: &mut Ui) {
ui.label("\n");
}
pub fn bullet_point(ui: &mut Ui) {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(width_body_space(ui) * 4.0, height_body(ui)),
Sense::hover(),
);
ui.painter().circle_filled(
rect.center(),
rect.height() / 6.0,
ui.visuals().strong_text_color(),
);
}
pub fn bullet_point_hollow(ui: &mut Ui) {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(width_body_space(ui) * 4.0, height_body(ui)),
Sense::hover(),
);
ui.painter().circle(
rect.center(),
rect.height() / 6.0,
egui::Color32::TRANSPARENT,
egui::Stroke::new(0.6, ui.visuals().strong_text_color()),
);
}
pub fn number_point(ui: &mut Ui, number: &str) {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(width_body_space(ui) * 4.0, height_body(ui)),
Sense::hover(),
);
ui.painter().text(
rect.right_center(),
egui::Align2::RIGHT_CENTER,
format!("{number}."),
TextStyle::Body.resolve(ui.style()),
ui.visuals().strong_text_color(),
);
}
#[inline]
pub fn footnote_start(ui: &mut Ui, note: &str) {
ui.label(RichText::new(note).raised().strong().small());
}
pub fn footnote(ui: &mut Ui, text: &str) {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(width_body_space(ui) * 4.0, height_body(ui)),
Sense::hover(),
);
ui.painter().text(
rect.right_top(),
egui::Align2::RIGHT_TOP,
format!("{text}."),
TextStyle::Small.resolve(ui.style()),
ui.visuals().strong_text_color(),
);
}
fn height_body(ui: &Ui) -> f32 {
ui.text_style_height(&TextStyle::Body)
}
fn width_body_space(ui: &Ui) -> f32 {
let id = TextStyle::Body.resolve(ui.style());
ui.fonts_mut(|f| f.glyph_width(&id, ' '))
}
fn paint_copy_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
let inset = (rect.width() * 0.18).max(2.0);
let front = rect.shrink(inset);
let back = front.translate(egui::vec2(-inset * 0.6, -inset * 0.6));
let rounding = egui::CornerRadius::same((inset * 0.4).round() as u8);
painter.rect_stroke(back, rounding, stroke, egui::StrokeKind::Inside);
painter.rect_stroke(front, rounding, stroke, egui::StrokeKind::Inside);
}
fn paint_check_icon(painter: &egui::Painter, rect: egui::Rect, stroke: egui::Stroke) {
let start = egui::pos2(rect.left() + rect.width() * 0.2, rect.center().y);
let mid = egui::pos2(
rect.center().x - rect.width() * 0.05,
rect.bottom() - rect.height() * 0.2,
);
let end = egui::pos2(
rect.right() - rect.width() * 0.18,
rect.top() + rect.height() * 0.2,
);
painter.line_segment([start, mid], stroke);
painter.line_segment([mid, end], stroke);
}
pub fn code_block<'t>(
ui: &mut Ui,
max_width: f32,
text: &str,
layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> std::sync::Arc<egui::Galley>,
) {
let mut text = text.strip_suffix('\n').unwrap_or(text);
let where_to_put_background = ui.painter().add(egui::Shape::Noop);
let output = egui::TextEdit::multiline(&mut text)
.layouter(layouter)
.desired_width(max_width)
.desired_rows(1)
.frame(egui::Frame::NONE)
.show(ui);
let frame_rect = output.response.rect;
let rounding = ui.visuals().widgets.inactive.corner_radius;
ui.painter().set(
where_to_put_background,
epaint::RectShape::new(
frame_rect,
rounding,
ui.visuals().extreme_bg_color,
ui.visuals().widgets.noninteractive.bg_stroke,
egui::StrokeKind::Outside,
),
);
let spacing = &ui.style().spacing;
let icon_size = spacing.icon_width;
let icon_rect = egui::Rect::from_min_size(
egui::pos2(
frame_rect.right_top().x - icon_size - spacing.button_padding.x,
frame_rect.right_top().y + spacing.button_padding.y,
),
egui::vec2(icon_size, icon_size),
);
let persistent_id = ui.make_persistent_id((output.response.id, "copy_button"));
let copied_icon = ui.memory_mut(|m| *m.data.get_temp_mut_or_default::<bool>(persistent_id));
let copy_button = ui
.interact(icon_rect, persistent_id, egui::Sense::click())
.on_hover_cursor(
ui.visuals()
.interact_cursor
.unwrap_or(egui::CursorIcon::Default),
);
let icon_stroke = egui::Stroke::new(1.2, ui.visuals().strong_text_color());
if copied_icon {
paint_check_icon(ui.painter(), icon_rect, icon_stroke);
} else {
paint_copy_icon(ui.painter(), icon_rect, icon_stroke);
}
if copied_icon && !copy_button.hovered() {
ui.memory_mut(|m| *m.data.get_temp_mut_or_default(persistent_id) = false);
}
if !copied_icon && copy_button.clicked() {
ui.memory_mut(|m| *m.data.get_temp_mut_or_default(persistent_id) = true);
}
if copy_button.clicked() {
use egui::TextBuffer as _;
let copy_text = if let Some(cursor) = output.cursor_range {
let selected_chars = cursor.as_sorted_char_range();
let selected_text = text.char_range(selected_chars);
if selected_text.is_empty() {
text.to_owned()
} else {
selected_text.to_owned()
}
} else {
text.to_owned()
};
ui.ctx().copy_text(copy_text);
}
}
pub struct ImmutableCheckbox<'a> {
checked: &'a mut bool,
}
impl<'a> ImmutableCheckbox<'a> {
pub fn without_text(checked: &'a mut bool) -> Self {
ImmutableCheckbox { checked }
}
}
impl egui::Widget for ImmutableCheckbox<'_> {
fn ui(self, ui: &mut Ui) -> egui::Response {
let ImmutableCheckbox { checked } = self;
let spacing = &ui.spacing();
let icon_width = spacing.icon_width;
let mut desired_size = egui::vec2(icon_width, 0.0);
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
desired_size.y = desired_size.y.max(icon_width);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
if ui.is_rect_visible(rect) {
let visuals = ui.style().visuals.noninteractive();
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
ui.painter().add(epaint::RectShape::new(
big_icon_rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.bg_fill,
visuals.bg_stroke,
egui::StrokeKind::Inside,
));
if *checked {
ui.painter().add(egui::Shape::line(
vec![
egui::pos2(small_icon_rect.left(), small_icon_rect.center().y),
egui::pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
egui::pos2(small_icon_rect.right(), small_icon_rect.top()),
],
visuals.fg_stroke,
));
}
}
response
}
}
pub fn blockquote(ui: &mut Ui, accent: egui::Color32, add_contents: impl FnOnce(&mut Ui)) {
let start = ui.painter().add(egui::Shape::Noop);
let response = egui::Frame::new()
.outer_margin(egui::Margin {
left: 10,
..Default::default()
})
.show(ui, add_contents)
.response;
ui.painter().set(
start,
egui::epaint::Shape::line_segment(
[
egui::pos2(response.rect.left_top().x, response.rect.left_top().y + 5.0),
egui::pos2(
response.rect.left_bottom().x,
response.rect.left_bottom().y - 5.0,
),
],
egui::Stroke::new(3.0, accent),
),
);
}