257 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
use std::ops::{Bound, RangeBounds};
 | 
						|
 | 
						|
use egui::{
 | 
						|
    ecolor::HexColor, Align, Color32, CursorIcon, Frame, Layout, Margin, Rect, Response, RichText,
 | 
						|
    Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
 | 
						|
};
 | 
						|
 | 
						|
pub trait UiExt {
 | 
						|
    fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
 | 
						|
    fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
 | 
						|
    fn selectable_option<T: Eq>(
 | 
						|
        &mut self,
 | 
						|
        current_value: &mut T,
 | 
						|
        selected_value: T,
 | 
						|
        text: impl Into<WidgetText>,
 | 
						|
    ) -> Response {
 | 
						|
        let response = self.selectable_button(*current_value == selected_value, text);
 | 
						|
        if response.clicked() {
 | 
						|
            *current_value = selected_value;
 | 
						|
        }
 | 
						|
        response
 | 
						|
    }
 | 
						|
 | 
						|
    fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
 | 
						|
 | 
						|
    fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
 | 
						|
}
 | 
						|
 | 
						|
impl UiExt for Ui {
 | 
						|
    fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui)) {
 | 
						|
        let mut frame = Frame::group(self.style());
 | 
						|
        frame.outer_margin.top += 10.0;
 | 
						|
        frame.inner_margin.top += 2.0;
 | 
						|
        let res = frame.show(self, add_contents);
 | 
						|
        let text = RichText::new(title).background_color(self.style().visuals.panel_fill);
 | 
						|
        let old_rect = res.response.rect;
 | 
						|
        let mut text_rect = old_rect;
 | 
						|
        text_rect.min.x += 6.0;
 | 
						|
        let new_rect = self
 | 
						|
            .allocate_new_ui(UiBuilder::new().max_rect(text_rect), |ui| ui.label(text))
 | 
						|
            .response
 | 
						|
            .rect;
 | 
						|
        self.allocate_space((old_rect.max - new_rect.max) - (old_rect.min - new_rect.min));
 | 
						|
    }
 | 
						|
 | 
						|
    fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
 | 
						|
        self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
 | 
						|
        self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
 | 
						|
        self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
 | 
						|
        let mut selected = selected;
 | 
						|
        self.checkbox(&mut selected, text)
 | 
						|
    }
 | 
						|
 | 
						|
    fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response {
 | 
						|
        let button_size = Vec2::new(60.0, 20.0);
 | 
						|
        let (rect, response) = self.allocate_at_least(button_size, Sense::click());
 | 
						|
        let center_x = rect.center().x;
 | 
						|
        let left_rect = rect.with_max_x(center_x);
 | 
						|
        self.painter().rect_filled(left_rect, 0.0, left);
 | 
						|
        let right_rect = rect.with_min_x(center_x);
 | 
						|
        self.painter().rect_filled(right_rect, 0.0, right);
 | 
						|
 | 
						|
        let style = self.style().interact(&response);
 | 
						|
        self.painter().rect_stroke(rect, 0.0, style.fg_stroke);
 | 
						|
        response
 | 
						|
    }
 | 
						|
 | 
						|
    fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response {
 | 
						|
        self.allocate_ui_with_layout(
 | 
						|
            Vec2::new(100.0, 130.0),
 | 
						|
            Layout::top_down_justified(egui::Align::Center),
 | 
						|
            |ui| {
 | 
						|
                let (rect, _) = ui.allocate_at_least(Vec2::new(100.0, 100.0), Sense::hover());
 | 
						|
                ui.painter().rect_filled(rect, 0.0, *color);
 | 
						|
                let resp = ui.text_edit_singleline(hex);
 | 
						|
                if resp.changed() {
 | 
						|
                    if let Ok(new_color) = HexColor::from_str_without_hash(hex) {
 | 
						|
                        *color = new_color.color();
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                resp
 | 
						|
            },
 | 
						|
        )
 | 
						|
        .inner
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
pub struct NumberEdit<'a> {
 | 
						|
    value: &'a mut usize,
 | 
						|
    min: Option<usize>,
 | 
						|
    max: Option<usize>,
 | 
						|
}
 | 
						|
 | 
						|
impl<'a> NumberEdit<'a> {
 | 
						|
    pub fn new(value: &'a mut usize) -> Self {
 | 
						|
        Self {
 | 
						|
            value,
 | 
						|
            min: None,
 | 
						|
            max: None,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    pub fn range(self, range: impl RangeBounds<usize>) -> Self {
 | 
						|
        let min = match range.start_bound() {
 | 
						|
            Bound::Unbounded => None,
 | 
						|
            Bound::Included(t) => Some(*t),
 | 
						|
            Bound::Excluded(t) => t.checked_add(1),
 | 
						|
        };
 | 
						|
        let max = match range.end_bound() {
 | 
						|
            Bound::Unbounded => None,
 | 
						|
            Bound::Included(t) => Some(*t),
 | 
						|
            Bound::Excluded(t) => t.checked_sub(1),
 | 
						|
        };
 | 
						|
        Self { min, max, ..self }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl Widget for NumberEdit<'_> {
 | 
						|
    fn ui(self, ui: &mut Ui) -> Response {
 | 
						|
        let (last_value, mut str) = ui.memory(|m| {
 | 
						|
            m.data
 | 
						|
                .get_temp(ui.id())
 | 
						|
                .unwrap_or((*self.value, self.value.to_string()))
 | 
						|
        });
 | 
						|
        let mut stale = false;
 | 
						|
        if *self.value != last_value {
 | 
						|
            str = self.value.to_string();
 | 
						|
            stale = true;
 | 
						|
        }
 | 
						|
        let valid = str.parse().is_ok_and(|v: usize| v == *self.value);
 | 
						|
        let text = TextEdit::singleline(&mut str)
 | 
						|
            .horizontal_align(Align::Max)
 | 
						|
            .margin(Margin {
 | 
						|
                left: 4.0,
 | 
						|
                right: 20.0,
 | 
						|
                top: 2.0,
 | 
						|
                bottom: 2.0,
 | 
						|
            });
 | 
						|
        let res = if valid {
 | 
						|
            ui.add(text)
 | 
						|
        } else {
 | 
						|
            let message = match (self.min, self.max) {
 | 
						|
                (Some(min), Some(max)) => format!("Please enter a number between {min} and {max}."),
 | 
						|
                (Some(min), None) => format!("Please enter a number greater than {min}."),
 | 
						|
                (None, Some(max)) => format!("Please enter a number less than {max}."),
 | 
						|
                (None, None) => "Please enter a number.".to_string(),
 | 
						|
            };
 | 
						|
            ui.scope(|ui| {
 | 
						|
                let style = ui.style_mut();
 | 
						|
                style.visuals.selection.stroke.color = style.visuals.error_fg_color;
 | 
						|
                style.visuals.widgets.hovered.bg_stroke.color =
 | 
						|
                    style.visuals.error_fg_color.gamma_multiply(0.60);
 | 
						|
                ui.add(text)
 | 
						|
            })
 | 
						|
            .inner
 | 
						|
            .on_hover_text(message)
 | 
						|
        };
 | 
						|
 | 
						|
        let arrow_left = res.rect.max.x + 4.0;
 | 
						|
        let arrow_right = res.rect.max.x + 20.0;
 | 
						|
        let arrow_top = res.rect.min.y - 2.0;
 | 
						|
        let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0;
 | 
						|
        let arrow_bottom = res.rect.max.y + 2.0;
 | 
						|
 | 
						|
        let mut delta = 0;
 | 
						|
        let top_arrow_rect = Rect {
 | 
						|
            min: (arrow_left, arrow_top).into(),
 | 
						|
            max: (arrow_right, arrow_middle).into(),
 | 
						|
        };
 | 
						|
        if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() {
 | 
						|
            delta = 1;
 | 
						|
        }
 | 
						|
        let bottom_arrow_rect = Rect {
 | 
						|
            min: (arrow_left, arrow_middle).into(),
 | 
						|
            max: (arrow_right, arrow_bottom).into(),
 | 
						|
        };
 | 
						|
        if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() {
 | 
						|
            delta = -1;
 | 
						|
        }
 | 
						|
 | 
						|
        let in_range =
 | 
						|
            |&val: &usize| self.min.is_none_or(|m| m <= val) && self.max.is_none_or(|m| m >= val);
 | 
						|
        if delta != 0 {
 | 
						|
            if let Some(new_value) = self.value.checked_add_signed(delta).filter(in_range) {
 | 
						|
                *self.value = new_value;
 | 
						|
            }
 | 
						|
            str = self.value.to_string();
 | 
						|
            stale = true;
 | 
						|
        } else if res.changed {
 | 
						|
            if let Some(new_value) = str.parse().ok().filter(in_range) {
 | 
						|
                *self.value = new_value;
 | 
						|
            }
 | 
						|
            stale = true;
 | 
						|
        }
 | 
						|
        if stale {
 | 
						|
            ui.memory_mut(|m| m.data.insert_temp(ui.id(), (*self.value, str)));
 | 
						|
        }
 | 
						|
        res
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response {
 | 
						|
    let arrow_res = ui
 | 
						|
        .allocate_rect(rect, Sense::click_and_drag())
 | 
						|
        .on_hover_cursor(CursorIcon::Default);
 | 
						|
    let visuals = ui.style().visuals.widgets.style(&arrow_res);
 | 
						|
    let painter = ui.painter_at(arrow_res.rect);
 | 
						|
 | 
						|
    let rounding = if up {
 | 
						|
        Rounding {
 | 
						|
            ne: 2.0,
 | 
						|
            ..Rounding::ZERO
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        Rounding {
 | 
						|
            se: 2.0,
 | 
						|
            ..Rounding::ZERO
 | 
						|
        }
 | 
						|
    };
 | 
						|
    painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill);
 | 
						|
 | 
						|
    let left = rect.left() + 4.0;
 | 
						|
    let center = (rect.left() + rect.right()) / 2.0;
 | 
						|
    let right = rect.right() - 4.0;
 | 
						|
    let top = rect.top() + 3.0;
 | 
						|
    let bottom = rect.bottom() - 3.0;
 | 
						|
    let points = if up {
 | 
						|
        vec![
 | 
						|
            (left, bottom).into(),
 | 
						|
            (center, top).into(),
 | 
						|
            (right, bottom).into(),
 | 
						|
        ]
 | 
						|
    } else {
 | 
						|
        vec![
 | 
						|
            (right, top).into(),
 | 
						|
            (center, bottom).into(),
 | 
						|
            (left, top).into(),
 | 
						|
        ]
 | 
						|
    };
 | 
						|
    painter.add(Shape::convex_polygon(
 | 
						|
        points,
 | 
						|
        visuals.fg_stroke.color,
 | 
						|
        Stroke::NONE,
 | 
						|
    ));
 | 
						|
    arrow_res
 | 
						|
}
 | 
						|
 | 
						|
trait ResponseExt {
 | 
						|
    fn clicked_or_dragged(&self) -> bool;
 | 
						|
}
 | 
						|
 | 
						|
impl ResponseExt for Response {
 | 
						|
    fn clicked_or_dragged(&self) -> bool {
 | 
						|
        self.clicked() || self.dragged()
 | 
						|
    }
 | 
						|
}
 |