1
0
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:
Emil Ernerfeldt
2020-04-23 19:15:17 +02:00
parent 25b06a6ff0
commit b39555bb23
18 changed files with 211 additions and 55 deletions

View File

@@ -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);

View File

@@ -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)> {

View File

@@ -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(

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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::*,

View File

@@ -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)
}

View File

@@ -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)]

View File

@@ -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 = &region.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),

View File

@@ -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);
}