mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 13:20:05 -04:00
Add support for hyperlinks
This commit is contained in:
@@ -36,3 +36,4 @@ pub const BLACK: Color = srgba(0, 0, 0, 255);
|
||||
pub const RED: Color = srgba(255, 0, 0, 255);
|
||||
pub const GREEN: Color = srgba(0, 255, 0, 255);
|
||||
pub const BLUE: Color = srgba(0, 0, 255, 255);
|
||||
pub const LIGHT_BLUE: Color = srgba(140, 160, 255, 255);
|
||||
|
||||
@@ -4,18 +4,6 @@ use parking_lot::Mutex;
|
||||
|
||||
use crate::{layout::align_rect, *};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CursorIcon {
|
||||
Default,
|
||||
ResizeNorthWestSouthEast,
|
||||
}
|
||||
|
||||
impl Default for CursorIcon {
|
||||
fn default() -> Self {
|
||||
CursorIcon::Default
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the input, style and output of all GUI commands.
|
||||
pub struct Context {
|
||||
/// The default style for new regions
|
||||
@@ -25,8 +13,7 @@ pub struct Context {
|
||||
pub(crate) memory: Mutex<Memory>,
|
||||
pub(crate) graphics: Mutex<GraphicLayers>,
|
||||
|
||||
/// Set each frame to what the mouse cursor should look like.
|
||||
pub cursor_icon: Mutex<CursorIcon>,
|
||||
pub output: Mutex<Output>,
|
||||
|
||||
/// Used to debug name clashes of e.g. windows
|
||||
used_ids: Mutex<HashMap<Id, Pos2>>,
|
||||
@@ -41,7 +28,7 @@ impl Clone for Context {
|
||||
input: self.input,
|
||||
memory: Mutex::new(self.memory.lock().clone()),
|
||||
graphics: Mutex::new(self.graphics.lock().clone()),
|
||||
cursor_icon: Mutex::new(self.cursor_icon.lock().clone()),
|
||||
output: Mutex::new(self.output.lock().clone()),
|
||||
used_ids: Mutex::new(self.used_ids.lock().clone()),
|
||||
}
|
||||
}
|
||||
@@ -55,11 +42,16 @@ impl Context {
|
||||
input: Default::default(),
|
||||
memory: Default::default(),
|
||||
graphics: Default::default(),
|
||||
cursor_icon: Default::default(),
|
||||
output: Default::default(),
|
||||
used_ids: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Useful for pixel-perfect rendering
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
(point * self.input.pixels_per_point).round() / self.input.pixels_per_point
|
||||
}
|
||||
|
||||
pub fn input(&self) -> &GuiInput {
|
||||
&self.input
|
||||
}
|
||||
@@ -73,10 +65,13 @@ impl Context {
|
||||
}
|
||||
|
||||
// TODO: move
|
||||
pub fn new_frame(&mut self, gui_input: GuiInput) {
|
||||
pub fn begin_frame(&mut self, gui_input: GuiInput) {
|
||||
self.used_ids.lock().clear();
|
||||
self.input = gui_input;
|
||||
*self.cursor_icon.lock() = CursorIcon::Default;
|
||||
}
|
||||
|
||||
pub fn end_frame(&self) -> Output {
|
||||
std::mem::take(&mut self.output.lock())
|
||||
}
|
||||
|
||||
pub fn drain_paint_lists(&self) -> Vec<(Rect, PaintCmd)> {
|
||||
|
||||
@@ -32,7 +32,7 @@ impl Emigui {
|
||||
self.ctx.fonts.texture()
|
||||
}
|
||||
|
||||
pub fn new_frame(&mut self, new_input: RawInput) {
|
||||
pub fn begin_frame(&mut self, new_input: RawInput) {
|
||||
if !self.last_input.mouse_down || self.last_input.mouse_pos.is_none() {
|
||||
self.ctx.memory.lock().active_id = None;
|
||||
}
|
||||
@@ -42,17 +42,17 @@ impl Emigui {
|
||||
|
||||
// TODO: avoid this clone
|
||||
let mut new_ctx = (*self.ctx).clone();
|
||||
new_ctx.new_frame(gui_input);
|
||||
new_ctx.begin_frame(gui_input);
|
||||
self.ctx = Arc::new(new_ctx);
|
||||
}
|
||||
|
||||
/// A region for the entire screen, behind any windows.
|
||||
pub fn background_region(&mut self) -> Region {
|
||||
let rect = Rect::from_min_size(Default::default(), self.ctx.input.screen_size);
|
||||
Region::new(self.ctx.clone(), Layer::Background, Id::background(), rect)
|
||||
pub fn end_frame(&mut self) -> (Output, PaintBatches) {
|
||||
let output = self.ctx.end_frame();
|
||||
let paint_batches = self.paint();
|
||||
(output, paint_batches)
|
||||
}
|
||||
|
||||
pub fn paint(&mut self) -> PaintBatches {
|
||||
fn paint(&mut self) -> PaintBatches {
|
||||
self.mesher_options.aa_size = 1.0 / self.last_input.pixels_per_point;
|
||||
let paint_commands = self.ctx.drain_paint_lists();
|
||||
let batches = mesh_paint_commands(&self.mesher_options, &self.ctx.fonts, paint_commands);
|
||||
@@ -65,6 +65,14 @@ impl Emigui {
|
||||
batches
|
||||
}
|
||||
|
||||
/// A region for the entire screen, behind any windows.
|
||||
pub fn background_region(&mut self) -> Region {
|
||||
let rect = Rect::from_min_size(Default::default(), self.ctx.input.screen_size);
|
||||
Region::new(self.ctx.clone(), Layer::Background, Id::background(), rect)
|
||||
}
|
||||
}
|
||||
|
||||
impl Emigui {
|
||||
pub fn ui(&mut self, region: &mut Region) {
|
||||
region.collapsing("Style", |region| {
|
||||
region.add(Checkbox::new(
|
||||
|
||||
@@ -43,6 +43,11 @@ impl ExampleApp {
|
||||
region.add(label!(
|
||||
"Emigui is an experimental immediate mode GUI written in Rust."
|
||||
));
|
||||
|
||||
region.horizontal(Align::Min, |region| {
|
||||
region.add_label("Project home page:");
|
||||
region.add_hyperlink("https://github.com/emilk/emigui/");
|
||||
});
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Widgets")
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Font {
|
||||
(point * self.pixels_per_point).round() / self.pixels_per_point
|
||||
}
|
||||
|
||||
/// In points
|
||||
/// Height of one line of text. In points
|
||||
pub fn line_spacing(&self) -> f32 {
|
||||
self.scale_in_pixels / self.pixels_per_point
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ pub use {
|
||||
crate::emigui::Emigui,
|
||||
collapsing_header::CollapsingHeader,
|
||||
color::Color,
|
||||
context::{Context, CursorIcon},
|
||||
context::Context,
|
||||
fonts::{FontDefinitions, Fonts, TextStyle},
|
||||
id::Id,
|
||||
layers::*,
|
||||
|
||||
@@ -109,6 +109,10 @@ impl Region {
|
||||
.extend(cmds.drain(..).map(|cmd| (clip_rect, cmd)));
|
||||
}
|
||||
|
||||
pub fn round_to_pixel(&self, point: f32) -> f32 {
|
||||
self.ctx.round_to_pixel(point)
|
||||
}
|
||||
|
||||
/// Options for this region, and any child regions we may spawn.
|
||||
pub fn style(&self) -> &Style {
|
||||
&self.style
|
||||
@@ -183,7 +187,7 @@ impl Region {
|
||||
|
||||
// draw a grey line on the left to mark the region
|
||||
let line_start = child_rect.min() - indent * 0.5;
|
||||
let line_start = line_start.round();
|
||||
let line_start = line_start.round(); // TODO: round to pixel instead
|
||||
let line_end = pos2(line_start.x, line_start.y + size.y - 8.0);
|
||||
self.add_paint_cmd(PaintCmd::Line {
|
||||
points: vec![line_start, line_end],
|
||||
@@ -272,7 +276,7 @@ impl Region {
|
||||
Rect::from_min_max(pos, pos2(pos.x + column_width, self.desired_rect.bottom()));
|
||||
|
||||
Region {
|
||||
id: self.make_child_region_id(&("column", col_idx)),
|
||||
id: self.make_child_id(&("column", col_idx)),
|
||||
dir: Direction::Vertical,
|
||||
..self.child_region(child_rect)
|
||||
}
|
||||
@@ -303,6 +307,10 @@ impl Region {
|
||||
self.add(Label::new(text))
|
||||
}
|
||||
|
||||
pub fn add_hyperlink(&mut self, url: impl Into<String>) -> GuiResponse {
|
||||
self.add(Hyperlink::new(url))
|
||||
}
|
||||
|
||||
pub fn collapsing<S, F>(&mut self, text: S, add_contents: F) -> GuiResponse
|
||||
where
|
||||
S: Into<String>,
|
||||
@@ -365,7 +373,7 @@ impl Region {
|
||||
self.id.with(&Id::from_pos(self.cursor))
|
||||
}
|
||||
|
||||
pub fn make_child_region_id<H: Hash>(&self, child_id: &H) -> Id {
|
||||
pub fn make_child_id<H: Hash>(&self, child_id: &H) -> Id {
|
||||
self.id.with(child_id)
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,28 @@ impl GuiInput {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize)]
|
||||
pub struct Output {
|
||||
pub cursor_icon: CursorIcon,
|
||||
/// If set, open this url.
|
||||
pub open_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CursorIcon {
|
||||
Default,
|
||||
/// Pointing hand, used for e.g. web links
|
||||
PointingHand,
|
||||
ResizeNwSe,
|
||||
}
|
||||
|
||||
impl Default for CursorIcon {
|
||||
fn default() -> Self {
|
||||
CursorIcon::Default
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize)]
|
||||
|
||||
@@ -59,6 +59,62 @@ impl Widget for Label {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Hyperlink {
|
||||
url: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl Hyperlink {
|
||||
pub fn new(url: impl Into<String>) -> Self {
|
||||
let url = url.into();
|
||||
Self {
|
||||
text: url.clone(),
|
||||
url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Hyperlink {
|
||||
fn add_to(self, region: &mut Region) -> GuiResponse {
|
||||
let color = color::LIGHT_BLUE;
|
||||
let text_style = TextStyle::Body;
|
||||
let id = region.make_child_id(&self.url);
|
||||
let font = ®ion.fonts()[text_style];
|
||||
let line_spacing = font.line_spacing();
|
||||
// TODO: underline
|
||||
let (text, text_size) = font.layout_multiline(&self.text, region.available_width());
|
||||
let interact = region.reserve_space(text_size, Some(id));
|
||||
if interact.hovered {
|
||||
region.ctx().output.lock().cursor_icon = CursorIcon::PointingHand;
|
||||
}
|
||||
if interact.clicked {
|
||||
region.ctx().output.lock().open_url = Some(self.url.clone());
|
||||
}
|
||||
|
||||
if interact.hovered {
|
||||
// Underline:
|
||||
for fragment in &text {
|
||||
let pos = interact.rect.min();
|
||||
let y = pos.y + fragment.y_offset + line_spacing;
|
||||
let y = region.round_to_pixel(y);
|
||||
let min_x = pos.x + fragment.min_x();
|
||||
let max_x = pos.x + fragment.max_x();
|
||||
region.add_paint_cmd(PaintCmd::Line {
|
||||
points: vec![pos2(min_x, y), pos2(max_x, y)],
|
||||
color,
|
||||
width: region.style().line_width,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
region.add_text(interact.rect.min(), text_style, text, Some(color));
|
||||
|
||||
region.response(interact)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct Button {
|
||||
text: String,
|
||||
text_color: Option<Color>,
|
||||
@@ -416,7 +472,7 @@ impl<'a> Widget for Slider<'a> {
|
||||
let value = self.get_value_f32();
|
||||
|
||||
let rect = interact.rect;
|
||||
let rail_radius = (height / 8.0).round().max(2.0);
|
||||
let rail_radius = region.round_to_pixel((height / 8.0).max(2.0));
|
||||
let rail_rect = Rect::from_min_max(
|
||||
pos2(interact.rect.left(), rect.center().y - rail_radius),
|
||||
pos2(interact.rect.right(), rect.center().y + rail_radius),
|
||||
|
||||
@@ -139,6 +139,8 @@ impl Window {
|
||||
}
|
||||
};
|
||||
|
||||
state.outer_pos = state.outer_pos.round(); // TODO: round to pixel
|
||||
|
||||
let min_inner_size = self.min_size;
|
||||
let max_inner_size = self
|
||||
.max_size
|
||||
@@ -227,16 +229,16 @@ impl Window {
|
||||
state.outer_pos += ctx.input().mouse_move;
|
||||
}
|
||||
|
||||
if corner_interact.hovered || corner_interact.active {
|
||||
ctx.output.lock().cursor_icon = CursorIcon::ResizeNwSe;
|
||||
}
|
||||
|
||||
state = State {
|
||||
outer_pos: state.outer_pos,
|
||||
inner_size: new_inner_size,
|
||||
outer_rect: outer_rect,
|
||||
};
|
||||
|
||||
if corner_interact.hovered || corner_interact.active {
|
||||
*ctx.cursor_icon.lock() = CursorIcon::ResizeNorthWestSouthEast;
|
||||
}
|
||||
|
||||
if win_interact.active || corner_interact.active || mouse_pressed_on_window(ctx, id) {
|
||||
ctx.memory.lock().move_window_to_top(id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user