imgui
imgui copied to clipboard
Add a "HyperLink" control (clickable text with link-like styling)
Version/Branch of Dear ImGui:
Version 1.90.7, Branch: master
Back-ends:
Any (Unity/C# integration most recent case)
Compiler, OS:
Any (Windows, Unity C# 2022.x in most recent case)
Full config/build information:
No response
Details:
Feature Request
A small control that I've written for past projects and which we implemented at Wargaming, Blizzard (SGE), and at my current employer is a "HyperLink" widget, so it seems to me to be a frequently recurring pattern.
A hyperlink is some Text but which is rendered in a different color (e.g. blue) and has an underline, changes color slightly when hovered, and responds to clicks like a button, etc.
For users, this would be something like:
if (ImGui::HyperLink("Some label##id"))
DoThing();
Writing the most basic version of this is almost trivial, of course, but it gets a bit trickier once things like keyboard focus and activation are considered, and really tricky to handle keyboard focus styling well. Every single implementation of such a control that I've seen (including mine!) got some or all of those wrong initially. I'm still not sure that I've seen one that handles word-wrap and focus highlight correctly, now that I think about it.
Having something like this in dear imgui itself instead of being reimplemented (often poorly) would be a small win.
Screenshots/Video:
Example of what a HyperLink control might look like:
And focused (styling could be better, just a quick demo here):
Minimal, Complete and Verifiable Example code:
// Crappy HyperLink implemented in a few minutes
//
// Does not handle: wordwrap
// Focus handling is not quite right (focus item stays after clicking with the mouse; ideally would be only outlined if keyboard was used to select/activate most recently)
// More input options for buttons, etc.
// Lacks custom style options. uses hard-coded colors
// Lacks a flag or style/color for "visited" links
// Lacks disabled link style/color
// Option to only show underline on hover could be a style? Or part of flags
bool ImGui::HyperLink(const char* label, bool underlineWhenHoveredOnly = false)
{
const ImU32 linkColor = ImGui::ColorConvertFloat4ToU32({0.2, 0.3, 0.8, 1});
const ImU32 linkHoverColor = ImGui::ColorConvertFloat4ToU32({0.4, 0.6, 0.8, 1});
const ImU32 linkFocusColor = ImGui::ColorConvertFloat4ToU32({0.6, 0.4, 0.8, 1});
const ImGuiID id = ImGui::GetID(label);
ImGuiWindow* const window = ImGui::GetCurrentWindow();
ImDrawList* const draw = ImGui::GetWindowDrawList();
const ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
const ImVec2 size = ImGui::CalcTextSize(label);
ImRect bb(pos, {pos.x + size.x, pos.y + size.y});
ImGui::ItemSize(bb, 0.0f);
if (!ImGui::ItemAdd(bb, id))
return false;
bool isHovered = false;
const bool isClicked = ImGui::ButtonBehavior(bb, id, &isHovered, nullptr);
const bool isFocused = ImGui::IsItemFocused();
const ImU32 color = isHovered ? linkHoverColor : isFocused ? linkFocusColor : linkColor;
draw->AddText(bb.Min, color, label);
if (isFocused)
draw->AddRect(bb.Min, bb.Max, color);
else if (!underlineWhenHoveredOnly || isHovered)
draw->AddLine({ bb.Min.x, bb.Max.y }, bb.Max, color);
return isClicked;
}
// Test code that generated the screenshot
if (ImGui::Begin("Test Hyperlink"))
{
static bool clicked = false;
ImGui::Text("%s", clicked ? "YES" : "NO");
ImGui::Text("This is a link."); ImGui::SameLine();
if (ImGuiHyperLink("Click me!"))
clicked = !clicked;
if (ImGui::BeginItemTooltip())
{
ImGui::Text("Click to toggle");
ImGui::EndTooltip();
}
ImGui::SameLine(); ImGui::Text("That was a link.");
}
ImGui::End();
I am puzzled because I thought there was a topic or PR exactly about this but I can't find one now.
Also note TextURL() in https://gist.github.com/dougbinks/ef0962ef6ebe2cadae76c4e9f0586c69 for another reference (and generally https://github.com/ocornut/imgui/wiki/Useful-Extensions#rich-text).
Having something like this in dear imgui itself instead of being reimplemented (often poorly) would be a small win.
You are right.
The reasons something like this is not in yet:
- I've been shy of adding too many new colors in the style system, while the expectation is that this will be boldly reworked. Admittedly this is a bit of a procrastination tactic at this point.
- It'd be awkward to implement/demo an "open browser" thing at least in the demo as that's quite OS dependent. And it may be nice if links would open without user need to wrap link+action, but that's optional. See
ImOsOpenInShell()in https://github.com/ocornut/imgui_test_engine/blob/main/imgui_test_engine/imgui_te_utils.cpp. I wonder if if we should add this to dear imgui behind a function pointer stored inImGuiIO, defaulting to a internal version ofImOsOpenInShellbut letting user override. - It'd been hoping to include this as part of a larger rework of text functions, so it would be easily to include in wrapping text, markups etc. But arguably we would likely still need a helper API entry point similar to one you suggested.
I'm going to keep this open and see if we can implement something simple.
I've been shy of adding too many new colors in the style system, while the expectation is that this will be boldly reworked. Admittedly this is a bit of a procrastination tactic at this point.
Hah, alright. Perhaps a version that takes colors as parameters, a la TextColored, to start? (Though using the standard style system would be better ofc.)
It'd be awkward to implement/demo an "open browser" thing at least in the demo as that's quite OS dependent
A significant number of uses here have nothing to do with browers, but rather just replacing buttons for navigating within the app itself. That is, this is just a differently-styled button.
It certainly would be awesome to have an OpenExternal kind of thing in imgui, I think that's entirely orthogonal.
A control that's actually built around opening links in browsers is slightly different in other ways. It needs both URL and optional label parameters. It needs default behaviours to show the URL in a tooltip (assuming the label is not omitted). It needs some way to easily copy the URL to clipboard, e.g. via a context menu. That's a lot more stuff and a lot more "opinion" the that a URL/browser-focused control needs to provide over the core UI functionality of having clickable hyperlinks.
It'd been hoping to include this as part of a larger rework of text functions, so it would be easily to include in wrapping text, markups etc. But arguably we would likely still need a helper API entry point similar to one you suggested.
👍🏼 Some work I'm doing right now on an experimental "advancing layout+styling in imgui" project makes me think you're right about that. Breaking up text into separate runs across line breaks, with each run having its own hitbox/etc. makes handling advanced behavior w/ wordwrap much easier. Leaving all the wordwrap to the DrawList limits flexibility substantially.
Some work I'm doing right now on an experimental "advancing layout+styling in imgui" project makes me think you're right about that. Breaking up text into separate runs across line breaks, with each run having its own hitbox/etc. makes handling advanced behavior w/ wordwrap much easier. Leaving all the wordwrap to the DrawList limits flexibility substantially.
I have work in progress new text functions which will be more flexible in many ways (and also faster) but they aren't ready yet. One common issue is #2313 that wrapping offset is conflating with start offset.
I have added a io.PlatformOpenInShellFn handler now 8f3679803
Added new widgets functions 5496050
bool TextLink(const char* label); // hyperlink text button, return true when clicked
void TextLinkOpenURL(const char* label, const char* url = NULL); // hyperlink text button, automatically open file/url when clicked
(right-click has a context menu with a Copy Link button)
Two things I'm not happy about:
- I intentionally exposed a single new style color, deriving others from this one (I prefer to do that for now, to avoid cluttering style)
- Calculation of font descent is probably unperfect. To minimize issue I made the underline less saturated and rendered before the text.
Glad we have this in!
@juliettef @dougbinks @mekhontsev: note the underlying io.PlatformOpenInShellFn handler added by 8f367980 which should be a good standard (from #if (IMGUI_VERSION_NUM >= 19092)) to use. Default is implemented in PlatformOpenInShellFn_DefaultImpl() in imgui.cpp
@floooh @ypujante Feedback welcome on making this handler work better on all platforms, in particular I am curious if we can get this to work on Emscripten?
@ocornut As far as I know there is no built-in function to call in emscripten proper, but it is super easy to add one (feel free to name whatever makes sense...). Being in a browser environment in the first place makes the code trivial...
#ifdef __EMSCRIPTEN__
EM_JS(void, open_url, (char const *url), {
url = url ? UTF8ToString(url): null;
if(url)
window.open(url, '_blank');
});
#endif
// example usage:
if (ImGui::Button("Open URL"))
open_url("https://github.com/ocornut/imgui");
I don't know how you would translate this into the "handler" api/implementation, but this is the code you need to open a URL in emscripten...
Thanks! Added and tested the Emscripten handlers in GLFW and SDL backends now: 380b355
Happy to help. Let me know if you have any issues.
This is what I do to open a link in a new browser tab, it's essentially the same as above:
EM_JS(void, emsc_js_open_link, (const char* c_url), {
var url = UTF8ToString(c_url);
window.open(url);
});
But: IIRC it is important to call this code from within a HTML input event handler (or equivalent from within an Emscripten event callback) - more on that below.
...I'm using this here (go to "Help => About": https://floooh.github.io/visual6502remix/). The About-box content is rendered with imgui-markdown which has support for links.
The way link-clicks are handled is awkward though because of the above limitation that tabs can only be opened from within an HTML event handler:
I'm hooking into ImGuiMarkdown's "link-clicked callback" (which seems to be called when the mouse button is pressed down over the link) and only record the information that a link was clicked and its url. Then in the sokol_app.h mouse-up event handler (which is called from within the HTML event handler) I check that flag and call emsc_js_open_link().
PS: @ypujante I'm very surprised that this works because of the above mentioned limitation that browser tabs can only be opened from within HTML event handlers:
// example usage:
if (ImGui::Button("Open URL"))
open_url("https://github.com/ocornut/imgui");
Because (from: https://developer.mozilla.org/en-US/docs/Web/API/Window/open):
I'm hooking into ImGuiMarkdown's "link-clicked callback" (which seems to be called when the mouse button is pressed down over the link)
This should be called when the mouse button is released:
https://github.com/enkisoftware/imgui_markdown/blob/main/imgui_markdown.h#L852-L862
@dougbinks Hmm indeed, now I'm actually wondering why it works :D
My link-clicked callback looks like this:
https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/src/ui.cc#L2032-L2046
...this just records that a link has been clicked...
...and then in the sokol_app.h event handler I'm handling that before even passing input events to Dear ImGui (this test_click() function checks for mouse-button-up):
https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/src/ui.cc#L520-L525
...and only further down I'm passing input events to Dear ImGui:
https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/src/ui.cc#L616-L621
...need to investigate, one sec...
@dougbinks: mystery solved, I actually changed imgui_markdown.h so that the callback is always called as soon as it is hovered:
https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/ext/imgui_markdown/imgui_markdown.h#L516-L526
(also this is a very old version of imgui_markdown.h)
Afaik in my test it worked on Firefox but it was from localhost:8000 and i don't know if there are varying policies for this.
Yeah it's a mess tbh (like many things on the web platform unfortunately), serving from localhost typically enables a couple of security-related features to make testing easier. But I don't know if opening a new browser tab via window.open() is one of those, it could also be a Firefox thing.
@floooh I tried with 3 different browsers on my desktop (macOS): Firefox / Chrome and Safari. I use Chrome and Firefox all the time. Rarely Safari. Firefox and Chrome opened the link with no issue. Safari blocked the popup window, but displayed a message about it with a button to allow it: I clicked it and it worked. I probably have allowed popups from localhost on Firefox and Chrome a while back since I use them for development a lot
But I believe Safari is the "normal" behavior: browsers will more likely block the popup but will let you know about it and you can override it.
Hmm yeah, I seem to remember that there's just a subtle icon that a "popup" has been blocked when serving from real web servers, and clicking the icon may allow to define an exception, but this kind-of sucks because it looks like the application wants to do something fishy :)
I think it would still be good to allow applications to open the browser tab from a different place where the Dear ImGui link is defined, but I don't know how this could be best done without breaking the ImGui philosophy too much.
The way I solved it is basically to keep track if the currently hovered item is a link (and which link), because I need to know this information before the actual click-event on the link happens. Then in my own 'mouse button event handler' (which is running inside the HTML event handler) I check that earlier recorded 'is-link-hovered' flag and call window.open() with the (also recorded) link URL.
PS: this hover-check wouldn't work for touch UIs unfortunately :(
PPS: yeah, confirmed, my approach doesn't work on Android because it relies on the information that an item is hovered.
@floooh this is definitely one of the major drawbacks of Immediate GUI in general... in the browser everything is event based and that is a very different model...
The regular key- and pointer-input from browser to Dear ImGui works pretty well, it's mostly about special browser features which require to be called from a "short-lived event handler" for security reasons. So far I stumbled over this issue for:
- clipboard features (exceptionally gnarly in browsers)
- opening tabs
- activating web audio contexts
- switching to fullscreen
- activating pointer-lock
...also slightly unrelated, text input on mobile devices is its own special circle of hell, I simply gave up on that.
For the link-input I can think of a model that could work also on mobile though, instead of recording 'hover' I could probably record whether a touch-start event happens over a link, and then in the following associated touch-end event try to open the tab. Similar with mouse-down / mouse-up. I wonder why I didn't try that - or maybe I tried it and it didn't work for some reason :/
I'm not sure I understand the problem discussed above. May I close this issue? Is there something that you think should be done on imgui or imgui's backends end?
Regarding
https://github.com/ocornut/imgui/issues/7660#issuecomment-2203697098
Did you try if it also works on a 'properly hosted' web page hosted via https and not just from localhost, 127.0.0.1 or 0.0.0.0?
Normally a link can only be opened in web browsers from within an HTML input event handler, so io.PlatformOpenInShellFn would need to be called directly from within an Emscripten event callback. I don't think the Dear ImGui input system is compatible with that restriction, but at the same time it's probably not worth it to do any deep changes just to make this particular feature work.
I didn't, no. I tried on localhost.
Would this triggers an authorization popup the first time, or would this just fail? If the later we should definitively dig into the workaround.
I do have a "recent" build not hosted on localhost if you want to try https://pongasoft.github.io/imgui/pr-7647/use_port_contrib_glfw3/ (requires webgpu support). From my experiece clicking on the links work...
It does work on Chrome indeed.
Nice, yeah works here too :)
@ypujante Is that the regular example_glfw_wgpu in the Dear ImGui repo? I need to check how that magic works ;)
@ocornut AFAIK if there would be an old popup blocker exception active, Chrome would show some sort of icon in the navbar. I don't see one though, so I guess it works without a one-time popup blocker exception.
In any case, from my pov it's ok to close this issue :)
Ah ok, as always, Safari is the problem... I get a 'Popup Blocked" message there and nothing happens (since the demo requires WebGPU, need to test with the latest Safari Technology Preview version, and AFAIK a WebGPU flags needs to be enabled).
(while my solution in https://floooh.github.io/visualz80remix/ works with Safari since it opens the tab from within an Emscripten event callback)
@floooh can you point me to the code that does: "works with Safari since it opens the tab from within an Emscripten event callback"? I would like to see if I can implement it in my library. Thank you.
@ypujante it's a bit of a mess tbh and spread over different places in the code.
Best to start here in my event handler and work backwards. The event handler is called from within Emscripten event callbacks, which in turn run inside a HTML event handler (that's why the whole thing works also in Safari):
https://github.com/floooh/v6502r/blob/2c3dd083b2087577a9a49364c2822564cc2dfc52/src/ui.cc#L520-L525
That util_html5_open_link function does the JS window.open as usual.
The gist is that inside the regular UI description code I keep track of whether the mouse currently hovers an URL link, and what the URL is... and then defer actually opening the link to my own event handler when a mouse click happens and the mouse currently hovers an URL link, so opening the link is not triggered by Dear ImGui, I'm only "polling" Dear ImGui each frame if the mouse hovers a link basically.
In reality I had to hack around a bit in imgui_markdown to call its 'link clicked' callback not only on a click, but each frame as soon as the mouse hovers the link. This was necessary to resolve the chicken-egg-situation that I need to open the the URL from within my own input handler on a click even before Dear ImGui knows about this click, I need to know whether a link needs to be opened before the click happens.
Maybe a similar 'hack' is required in Dear ImGui to make it work with the new PlatformOpenInShellFn, or maybe the per-frame 'link hovered' check is better done outside Dear ImGui (in that case that new PlatformOpenInShellFn callback is kinda useless though.
...or maybe alternatively Dear ImGui could offer a function const char* TextLinkHovered() which can be called at any point in the frame and either returns a nullptr if no link is hovered, or the link URL of the hovered link. This function could then be called inside a HTML clicked handler to decide if a tab needs to be opened (this would require that Dear ImGui needs to store the link URL though).
Thank you for the link and explanation. I will investigate and see if I can come up with something generic for emscripten-glfw and ImGui.
TBH I am having somewhat similar issue with get clipboard due to the asynchronous nature of the api: I implemented an extension API (asynchronous) but it is hard to retrofit it in ImGui (since the Paste keyboard event triggers the asynchronous call and only when it happens can you actually apply the paste...)
Yeah I have the same sorts of hacks for clipboard support on the web. When you look around in the code near the region I linked above, you'll see :D
@floooh I wanted to let you know that I have a "clean" solution for the "Popup Blocked" Safari issue and that it will be released with the next version of emscripten_glfw with a new convenient API:
// cpp API
/**
* Convenient call to open a url */
void OpenURL(std::string_view url, std::optional<std::string_view> target = "_blank");
// C API
/**
* Convenient call to open a url
* @param target can be `nullptr` which defaults to `_blank` */
void emscripten_glfw_open_url(char const *url, char const *target);
it will just be a matter of setting this function something like this in ImGui (I will do a PR when ready):
#ifdef __EMSCRIPTEN__
#if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 > [versionTBD]
void ImGui_ImplGlfw_EmscriptenOpenURL(char const* url)
{
if(url) emscripten_glfw_open_url(url, nullptr);
}
#else
EM_JS(void, ImGui_ImplGlfw_EmscriptenOpenURL, (char const* url), { url = url ? UTF8ToString(url) : null; if (url) window.open(url, '_blank'); });
#endif
#endif