Tooltip on Hover
Tooltips provide contextual information when hovering over UI elements. This recipe shows how to use on_enter/on_exit/on_move mouse callbacks to show and hide tooltip overlays.
The Pattern
Every drawable (Frame, Caption, Sprite, etc.) has native hover callbacks — no click-hijacking or manual polling required:
element.on_enter = handler— called with(pos: Vector)when the mouse enters the element’s boundselement.on_exit = handler— called with(pos: Vector)when the mouse leaves the element’s boundselement.on_move = handler— called with(pos: Vector)on every mouse movement while inside the bounds
Separately, element.on_click = handler fires on button press/release and receives (pos: Vector, button: MouseButton, action: InputState). Clicks and hover are independent callbacks — you don’t need to inspect on_click to detect hovering.
There’s also a read-only element.hovered property if you’d rather poll state from a Timer than register callbacks.
Basic Tooltip Implementation
import mcrfpy
class Tooltip:
"""A simple tooltip that follows the mouse."""
def __init__(self, text, width=200):
"""
Create a tooltip (initially hidden).
Args:
text: Tooltip text
width: Maximum width
"""
self.text = text
self.visible = False
# Tooltip frame
self.frame = mcrfpy.Frame(pos=(0, 0), size=(width, 30))
self.frame.fill_color = mcrfpy.Color(40, 40, 50, 240)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(100, 100, 120)
self.frame.visible = False
# Tooltip text
self.caption = mcrfpy.Caption(text=text, pos=(5, 5))
self.caption.fill_color = mcrfpy.Color(255, 255, 220)
self.caption.visible = False
def show(self, x, y):
"""Show tooltip at position."""
# Offset from cursor
self.frame.x = x + 15
self.frame.y = y + 15
self.caption.x = self.frame.x + 8
self.caption.y = self.frame.y + 6
# Keep on screen
if self.frame.x + self.frame.w > 1024:
self.frame.x = x - self.frame.w - 5
self.caption.x = self.frame.x + 8
if self.frame.y + self.frame.h > 768:
self.frame.y = y - self.frame.h - 5
self.caption.y = self.frame.y + 6
self.frame.visible = True
self.caption.visible = True
self.visible = True
def hide(self):
"""Hide the tooltip."""
self.frame.visible = False
self.caption.visible = False
self.visible = False
def set_text(self, text):
"""Update tooltip text."""
self.text = text
self.caption.text = text
def add_to_scene(self, ui):
"""Add tooltip to scene (add last for top layer)."""
ui.append(self.frame)
ui.append(self.caption)
def make_hoverable(element, tooltip, tooltip_text=None):
"""
Make a UI element show a tooltip on hover.
Args:
element: Frame, Caption, or Sprite to make hoverable
tooltip: Tooltip instance
tooltip_text: Text to show (optional, uses tooltip's default)
"""
def on_enter(pos):
if tooltip_text:
tooltip.set_text(tooltip_text)
tooltip.show(pos.x, pos.y)
def on_move(pos):
tooltip.show(pos.x, pos.y)
def on_exit(pos):
tooltip.hide()
element.on_enter = on_enter
element.on_move = on_move
element.on_exit = on_exit
# Usage Example
scene = mcrfpy.Scene("tooltip_demo")
mcrfpy.current_scene = scene
# Create some buttons
button1 = mcrfpy.Frame(pos=(100, 100), size=(120, 40))
button1.fill_color = mcrfpy.Color(60, 60, 100)
button1.outline = 2
button1.outline_color = mcrfpy.Color(100, 100, 150)
scene.children.append(button1)
btn1_label = mcrfpy.Caption(text="Attack", pos=(125, 112))
btn1_label.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(btn1_label)
button2 = mcrfpy.Frame(pos=(100, 160), size=(120, 40))
button2.fill_color = mcrfpy.Color(60, 100, 60)
button2.outline = 2
button2.outline_color = mcrfpy.Color(100, 150, 100)
scene.children.append(button2)
btn2_label = mcrfpy.Caption(text="Defend", pos=(125, 172))
btn2_label.fill_color = mcrfpy.Color(255, 255, 255)
scene.children.append(btn2_label)
# Create shared tooltip (add last for top layer)
tooltip = Tooltip("", width=250)
tooltip.add_to_scene(scene.children)
# Make buttons hoverable
make_hoverable(button1, tooltip, "Deal damage to the enemy")
make_hoverable(button2, tooltip, "Reduce incoming damage by 50%")
Because on_enter/on_exit fire directly from the engine’s mouse tracking, there’s no need for a polling Timer just to detect “mouse left the element” — on_exit already tells you that.
Tooltip Manager for Multiple Elements
A more robust system for managing tooltips across many elements, with a hover delay before the tooltip appears:
import mcrfpy
class TooltipManager:
"""Manages tooltips for multiple UI elements."""
def __init__(self):
self.elements = {} # id(element) -> {text, title} mapping
self.tooltip_frame = None
self.tooltip_text = None
self.current_element = None
self.hover_delay = 500 # ms before showing tooltip
self.pending_element = None
self.pending_x = 0
self.pending_y = 0
self._create_tooltip()
def _create_tooltip(self):
"""Create the tooltip UI elements."""
self.tooltip_frame = mcrfpy.Frame(pos=(0, 0), size=(200, 60))
self.tooltip_frame.fill_color = mcrfpy.Color(30, 30, 45, 245)
self.tooltip_frame.outline = 1
self.tooltip_frame.outline_color = mcrfpy.Color(80, 80, 100)
self.tooltip_frame.visible = False
self.tooltip_text = mcrfpy.Caption(text="", pos=(0, 0))
self.tooltip_text.fill_color = mcrfpy.Color(255, 255, 230)
self.tooltip_text.visible = False
def register(self, element, text, title=None):
"""
Register an element for tooltips.
Args:
element: UI element to track
text: Tooltip description
title: Optional title (shown in different color)
"""
self.elements[id(element)] = {
'element': element,
'text': text,
'title': title
}
def on_enter(pos):
self._on_element_enter(element, pos.x, pos.y)
def on_exit(pos):
self._on_element_exit(element)
element.on_enter = on_enter
element.on_exit = on_exit
def _on_element_enter(self, element, x, y):
"""Handle mouse entering a registered element."""
elem_id = id(element)
if elem_id not in self.elements:
return
self.pending_element = element
self.pending_x = x
self.pending_y = y
def show_after_delay(timer, runtime):
# Only show if the mouse is still over this element
if self.pending_element is element:
self._show_tooltip(element, self.pending_x, self.pending_y)
timer_name = f"tooltip_delay_{id(self)}"
mcrfpy.Timer(timer_name, show_after_delay, self.hover_delay)
def _on_element_exit(self, element):
"""Handle mouse leaving a registered element."""
if self.pending_element is element:
self.pending_element = None
if self.current_element is element:
self.hide()
def _show_tooltip(self, element, x, y):
"""Display tooltip for element."""
elem_id = id(element)
if elem_id not in self.elements:
return
data = self.elements[elem_id]
# Build tooltip content
content = ""
if data.get('title'):
content = data['title'] + "\n"
content += data['text']
# Update tooltip text
self.tooltip_text.text = content
# Calculate size based on content
lines = content.split('\n')
max_width = max(len(line) for line in lines) * 8 + 20
height = len(lines) * 18 + 15
self.tooltip_frame.w = min(300, max(100, max_width))
self.tooltip_frame.h = height
# Position tooltip
self.tooltip_frame.x = x + 20
self.tooltip_frame.y = y + 20
# Keep on screen
if self.tooltip_frame.x + self.tooltip_frame.w > 1000:
self.tooltip_frame.x = x - self.tooltip_frame.w - 10
if self.tooltip_frame.y + self.tooltip_frame.h > 750:
self.tooltip_frame.y = y - self.tooltip_frame.h - 10
self.tooltip_text.x = self.tooltip_frame.x + 10
self.tooltip_text.y = self.tooltip_frame.y + 8
# Show
self.tooltip_frame.visible = True
self.tooltip_text.visible = True
self.current_element = element
def hide(self):
"""Hide the tooltip."""
self.tooltip_frame.visible = False
self.tooltip_text.visible = False
self.current_element = None
self.pending_element = None
def add_to_scene(self, ui):
"""Add tooltip to scene (call last for top layer)."""
ui.append(self.tooltip_frame)
ui.append(self.tooltip_text)
# Usage
scene = mcrfpy.Scene("demo")
mcrfpy.current_scene = scene
# Create tooltip manager
tips = TooltipManager()
# Create some items with tooltips
sword_frame = mcrfpy.Frame(pos=(100, 100), size=(64, 64))
sword_frame.fill_color = mcrfpy.Color(80, 60, 40)
sword_frame.outline = 2
sword_frame.outline_color = mcrfpy.Color(120, 100, 80)
scene.children.append(sword_frame)
tips.register(
sword_frame,
"A rusty but reliable blade.\nDamage: 5-10\nSpeed: Fast",
title="Iron Sword"
)
shield_frame = mcrfpy.Frame(pos=(180, 100), size=(64, 64))
shield_frame.fill_color = mcrfpy.Color(60, 60, 80)
shield_frame.outline = 2
shield_frame.outline_color = mcrfpy.Color(100, 100, 120)
scene.children.append(shield_frame)
tips.register(
shield_frame,
"Blocks incoming attacks.\nDefense: +5\nWeight: Medium",
title="Steel Shield"
)
# Add tooltip last for top layer
tips.add_to_scene(scene.children)
Inline Tooltip (Attached to Element)
Sometimes you want tooltips that appear adjacent to elements rather than following the cursor:
import mcrfpy
def create_info_icon(x, y, tooltip_text, ui):
"""
Create an info icon that shows tooltip on hover.
Args:
x, y: Position of the icon
tooltip_text: Text to show
ui: Scene UI to add elements to
"""
# Info icon (small circle with "i")
icon = mcrfpy.Frame(pos=(x, y), size=(20, 20))
icon.fill_color = mcrfpy.Color(70, 130, 180)
icon.outline = 1
icon.outline_color = mcrfpy.Color(100, 160, 210)
icon_label = mcrfpy.Caption(text="i", pos=(x + 6, y + 2))
icon_label.fill_color = mcrfpy.Color(255, 255, 255)
# Tooltip (positioned to the right of icon)
tip_frame = mcrfpy.Frame(pos=(x + 25, y - 5), size=(180, 50))
tip_frame.fill_color = mcrfpy.Color(40, 40, 55, 240)
tip_frame.outline = 1
tip_frame.outline_color = mcrfpy.Color(80, 80, 100)
tip_frame.visible = False
tip_text = mcrfpy.Caption(text=tooltip_text, pos=(x + 33, y + 3))
tip_text.fill_color = mcrfpy.Color(220, 220, 220)
tip_text.visible = False
# Hover behavior — enter/exit fire directly, no polling needed
def on_icon_enter(pos):
tip_frame.visible = True
tip_text.visible = True
def on_icon_exit(pos):
tip_frame.visible = False
tip_text.visible = False
icon.on_enter = on_icon_enter
icon.on_exit = on_icon_exit
# Add to scene
ui.append(icon)
ui.append(icon_label)
ui.append(tip_frame)
ui.append(tip_text)
return icon
# Usage
scene = mcrfpy.Scene("info_demo")
mcrfpy.current_scene = scene
# Setting with info icon
setting_label = mcrfpy.Caption(text="Difficulty:", pos=(100, 100))
setting_label.fill_color = mcrfpy.Color(200, 200, 200)
scene.children.append(setting_label)
create_info_icon(200, 98, "Affects enemy\nHP and damage", scene.children)
McRogueFace-Specific Considerations
-
Hover Callbacks Are Native:
on_enter,on_exit, andon_moveare real per-element callbacks maintained by the engine’s mouse tracking — you don’t need to fake hover detection throughon_click, and you don’t need aTimerjust to notice the mouse left an element. -
hoveredProperty: Every drawable also exposes a read-onlyelement.hoveredboolean if you prefer to poll state from aTimerloop instead of registeringon_enter/on_exitcallbacks (useful when many elements share one piece of update logic). -
Z-Order for Tooltips: Always add tooltip frames last to ensure they render on top. If you add elements dynamically, you may need to remove and re-add the tooltip.
-
mcrfpy.automation.position(): Still useful for automated testing/screenshots, but not needed for ordinary hover detection now thaton_enter/on_exit/on_moveexist. -
Delay Timers Are Still Useful: A
Timerremains the right tool when you want a deliberate delay before showing a tooltip (as inTooltipManager.hover_delayabove) — that’s a UX choice, not a hover-detection workaround. -
Visibility Property: Use
element.visible = Falseto hide without removing from scene. This is more efficient than remove/append cycles.
Complete Example
import mcrfpy
scene = mcrfpy.Scene("game")
mcrfpy.current_scene = scene
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768))
bg.fill_color = mcrfpy.Color(25, 25, 35)
scene.children.append(bg)
class TooltipManager:
"""Manages tooltips for multiple UI elements."""
def __init__(self):
self.elements = {}
self.tooltip_frame = None
self.tooltip_text = None
self.current_element = None
self.hover_delay = 300
self.pending_element = None
self.pending_x = 0
self.pending_y = 0
self._create_tooltip()
def _create_tooltip(self):
self.tooltip_frame = mcrfpy.Frame(pos=(0, 0), size=(200, 60))
self.tooltip_frame.fill_color = mcrfpy.Color(30, 30, 45, 245)
self.tooltip_frame.outline = 1
self.tooltip_frame.outline_color = mcrfpy.Color(80, 80, 100)
self.tooltip_frame.visible = False
self.tooltip_text = mcrfpy.Caption(text="", pos=(0, 0))
self.tooltip_text.fill_color = mcrfpy.Color(255, 255, 230)
self.tooltip_text.visible = False
def register(self, element, text, title=None):
self.elements[id(element)] = {'element': element, 'text': text, 'title': title}
def on_enter(pos):
self.pending_element = element
self.pending_x, self.pending_y = pos.x, pos.y
def show_after_delay(timer, runtime):
if self.pending_element is element:
self._show_tooltip(element, self.pending_x, self.pending_y)
mcrfpy.Timer(f"tooltip_delay_{id(self)}", show_after_delay, self.hover_delay)
def on_exit(pos):
if self.pending_element is element:
self.pending_element = None
if self.current_element is element:
self.hide()
element.on_enter = on_enter
element.on_exit = on_exit
def _show_tooltip(self, element, x, y):
elem_id = id(element)
if elem_id not in self.elements:
return
data = self.elements[elem_id]
content = (data['title'] + "\n" if data.get('title') else "") + data['text']
self.tooltip_text.text = content
lines = content.split('\n')
max_width = max(len(line) for line in lines) * 8 + 20
height = len(lines) * 18 + 15
self.tooltip_frame.w = min(300, max(100, max_width))
self.tooltip_frame.h = height
self.tooltip_frame.x = x + 20
self.tooltip_frame.y = y + 20
if self.tooltip_frame.x + self.tooltip_frame.w > 1000:
self.tooltip_frame.x = x - self.tooltip_frame.w - 10
if self.tooltip_frame.y + self.tooltip_frame.h > 750:
self.tooltip_frame.y = y - self.tooltip_frame.h - 10
self.tooltip_text.x = self.tooltip_frame.x + 10
self.tooltip_text.y = self.tooltip_frame.y + 8
self.tooltip_frame.visible = True
self.tooltip_text.visible = True
self.current_element = element
def hide(self):
self.tooltip_frame.visible = False
self.tooltip_text.visible = False
self.current_element = None
self.pending_element = None
def add_to_scene(self, ui):
ui.append(self.tooltip_frame)
ui.append(self.tooltip_text)
# Create inventory slots with tooltips
class InventorySlot:
def __init__(self, x, y, item_name, item_desc, tooltip_mgr):
self.frame = mcrfpy.Frame(pos=(x, y), size=(50, 50))
self.frame.fill_color = mcrfpy.Color(50, 50, 60)
self.frame.outline = 1
self.frame.outline_color = mcrfpy.Color(80, 80, 90)
self.label = mcrfpy.Caption(text=item_name[:3], pos=(x + 10, y + 15))
self.label.fill_color = mcrfpy.Color(200, 200, 200)
tooltip_mgr.register(self.frame, item_desc, title=item_name)
def add_to_scene(self, ui):
ui.append(self.frame)
ui.append(self.label)
# Setup tooltip manager
tips = TooltipManager()
# Create inventory
items = [
("Health Potion", "Restores 50 HP\nConsumable"),
("Mana Crystal", "Restores 30 MP\nConsumable"),
("Iron Key", "Opens iron doors\nQuest Item"),
("Gold Ring", "Worth 100 gold\nSell to merchant"),
]
slots = []
for i, (name, desc) in enumerate(items):
slot = InventorySlot(100 + i * 60, 100, name, desc, tips)
slot.add_to_scene(scene.children)
slots.append(slot)
# Add tooltip last
tips.add_to_scene(scene.children)