animated example
This commit is contained in:
parent
216972e73a
commit
c9fd1b1585
17 changed files with 576 additions and 465 deletions
|
|
@ -30,9 +30,26 @@ module;
|
|||
#endif
|
||||
export module Crafter.Graphics:Device;
|
||||
import std;
|
||||
import :Types; // CrafterKeys for keyboard repeat state
|
||||
|
||||
export namespace Crafter {
|
||||
struct Window;
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
// Wayland's wl_keyboard.key only fires on real press/release — the
|
||||
// compositor expects the application to synthesize repeat events
|
||||
// itself using the rate/delay it advertises via wl_keyboard.repeat_info.
|
||||
struct KeyRepeatState {
|
||||
int rate = 25; // chars/sec
|
||||
int delay = 500; // ms before first repeat
|
||||
bool active = false;
|
||||
CrafterKeys key{};
|
||||
std::string utf8; // UTF-8 to re-emit as onTextInput, if any
|
||||
std::chrono::time_point<std::chrono::steady_clock> pressTime;
|
||||
std::chrono::time_point<std::chrono::steady_clock> lastFireTime;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct Device {
|
||||
static void Initialize();
|
||||
|
||||
|
|
@ -131,5 +148,18 @@ export namespace Crafter {
|
|||
|
||||
static void CheckVkResult(VkResult result);
|
||||
static std::uint32_t GetMemoryType(std::uint32_t typeBits, VkMemoryPropertyFlags properties);
|
||||
|
||||
// ─── Wayland key repeat ────────────────────────────────────────
|
||||
// TickKeyRepeats walks the held-key state and fires onKeyDown /
|
||||
// onTextInput accordingly. Called once per frame from
|
||||
// Window::Render. KeyRepeatState lives at namespace scope so its
|
||||
// member initializers don't trip C++'s "complete-type-needed"
|
||||
// rule for the inline static below.
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
inline static KeyRepeatState keyRepeat;
|
||||
static void TickKeyRepeats();
|
||||
#else
|
||||
static void TickKeyRepeats() {}
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ export namespace Crafter::UI {
|
|||
FontAtlas* atlas = nullptr;
|
||||
std::uint32_t bindlessBaseHeapIdx = 0; // base heap slot for Image widgets
|
||||
float scale = 1.0f; // device scale (mirrors LayoutContext::scale)
|
||||
float time = 0.0f; // seconds since scene init (drives blink etc.)
|
||||
|
||||
void Reset() { items.clear(); }
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ export namespace Crafter::UI {
|
|||
std::unique_ptr<EventListener<const std::string_view>> textListener_;
|
||||
std::unique_ptr<EventListener<CrafterKeys>> keyListener_;
|
||||
Widget* focused_ = nullptr;
|
||||
float elapsedSec_ = 0.0f;
|
||||
|
||||
float WindowScale() const;
|
||||
void RebuildFrame();
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ export namespace Crafter::UI {
|
|||
Font* font_ = nullptr;
|
||||
float size_ = 16.0f;
|
||||
Color color_{1, 1, 1, 1};
|
||||
Observable<std::string>* boundText_ = nullptr;
|
||||
|
||||
Text() = default;
|
||||
Text(std::string s) { runs_.emplace_back(std::move(s)); }
|
||||
|
|
@ -385,6 +386,11 @@ export namespace Crafter::UI {
|
|||
Text& size(float s) { size_ = s; return *this; }
|
||||
Text& color(Color c) { color_ = c; return *this; }
|
||||
|
||||
// Bind to an observable; the text is sourced from `obs` each frame
|
||||
// (overriding any `runs_`). Useful for live-updating labels driven
|
||||
// by a game-state observable.
|
||||
Text& bind(Observable<std::string>& obs) { boundText_ = &obs; return *this; }
|
||||
|
||||
// Replace the run list with a parameter pack of styled runs.
|
||||
template<typename... Rs>
|
||||
requires (std::convertible_to<std::decay_t<Rs>, TextRun> && ...)
|
||||
|
|
@ -398,6 +404,9 @@ export namespace Crafter::UI {
|
|||
Size Measure(Size avail, const LayoutContext& ctx) override {
|
||||
float ownW = ResolveLength(width_, avail.w, ctx.scale, [&]{
|
||||
if (!font_) return 0.0f;
|
||||
if (boundText_) {
|
||||
return static_cast<float>(font_->GetLineWidth(boundText_->Get(), size_ * ctx.scale));
|
||||
}
|
||||
float w = 0;
|
||||
for (auto& r : runs_) {
|
||||
float rs = (r.size_.value_or(size_)) * ctx.scale;
|
||||
|
|
@ -407,6 +416,9 @@ export namespace Crafter::UI {
|
|||
});
|
||||
float ownH = ResolveLength(height_, avail.h, ctx.scale, [&]{
|
||||
if (!font_) return 0.0f;
|
||||
if (boundText_) {
|
||||
return font_->LineHeight(size_ * ctx.scale);
|
||||
}
|
||||
// Tallest run dictates the line height.
|
||||
float h = 0;
|
||||
for (auto& r : runs_) {
|
||||
|
|
@ -425,6 +437,12 @@ export namespace Crafter::UI {
|
|||
|
||||
void Emit(DrawList& dl) const override {
|
||||
if (!font_) return;
|
||||
if (boundText_) {
|
||||
detail::EmitText(dl, font_, boundText_->Get(),
|
||||
size_ * dl.scale, color_,
|
||||
computedRect.x, computedRect.y);
|
||||
return;
|
||||
}
|
||||
float cursorX = computedRect.x;
|
||||
for (auto& r : runs_) {
|
||||
float rs = r.size_.value_or(size_) * dl.scale;
|
||||
|
|
@ -574,6 +592,7 @@ export namespace Crafter::UI {
|
|||
bool focused_ = false;
|
||||
std::size_t cursor_ = 0; // codepoint index within text_
|
||||
std::string placeholder_;
|
||||
float scrollX_ = 0.0f; // device-px offset to keep caret in view
|
||||
std::function<void(const std::string&)> onChange_;
|
||||
std::function<void(const std::string&)> onSubmit_;
|
||||
|
||||
|
|
@ -618,8 +637,32 @@ export namespace Crafter::UI {
|
|||
return desiredSize;
|
||||
}
|
||||
|
||||
void Arrange(Rect rect, const LayoutContext& /*ctx*/) override {
|
||||
void Arrange(Rect rect, const LayoutContext& ctx) override {
|
||||
computedRect = rect;
|
||||
|
||||
// Adjust horizontal scroll so the caret is always visible.
|
||||
if (font_) {
|
||||
EdgesPx p = ResolveEdges(padding_, ctx.scale);
|
||||
float visibleW = std::max(0.0f, rect.w - p.Horiz());
|
||||
float devSize = fontSize_ * ctx.scale;
|
||||
float caretW = std::max(1.0f, ctx.scale);
|
||||
|
||||
std::string_view before(text_.data(), cursor_);
|
||||
float prefixW = static_cast<float>(font_->GetLineWidth(before, devSize));
|
||||
|
||||
// If the caret has run off the right edge, scroll right.
|
||||
if (prefixW + caretW > scrollX_ + visibleW) {
|
||||
scrollX_ = prefixW + caretW - visibleW;
|
||||
}
|
||||
// If the caret has moved left of the visible window, scroll left.
|
||||
if (prefixW < scrollX_) {
|
||||
scrollX_ = prefixW;
|
||||
}
|
||||
// Don't scroll past the start; don't scroll past content end.
|
||||
float totalW = static_cast<float>(font_->GetLineWidth(text_, devSize));
|
||||
float maxScroll = std::max(0.0f, totalW - visibleW);
|
||||
scrollX_ = std::clamp(scrollX_, 0.0f, maxScroll);
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction. UIScene's focus manager flips `focused_` via
|
||||
|
|
@ -709,19 +752,36 @@ export namespace Crafter::UI {
|
|||
float devSize = fontSize_ * dl.scale;
|
||||
float originX = computedRect.x + p.left;
|
||||
float originY = computedRect.y + p.top;
|
||||
float visibleW = std::max(0.0f, computedRect.w - p.Horiz());
|
||||
float visibleH = std::max(0.0f, computedRect.h - p.Vert());
|
||||
|
||||
// Clip the text + caret to the content rect — once the
|
||||
// string overflows we slide it left under scrollX_, and
|
||||
// anything past the left/right edges must not draw outside
|
||||
// the field.
|
||||
dl.PushClip({originX, computedRect.y + p.top, visibleW, visibleH});
|
||||
|
||||
std::string_view show = !text_.empty() ? std::string_view(text_)
|
||||
: std::string_view(placeholder_);
|
||||
Color col = !text_.empty() ? textColor_
|
||||
: Color{textColor_.r, textColor_.g, textColor_.b, textColor_.a * 0.5f};
|
||||
detail::EmitText(dl, font_, show, devSize, col, originX, originY);
|
||||
detail::EmitText(dl, font_, show, devSize, col, originX - scrollX_, originY);
|
||||
|
||||
// Caret bar at the cursor position when focused.
|
||||
// Caret bar at the cursor position when focused. Blink at
|
||||
// ~530ms on / 530ms off (1.06s period) — standard rate
|
||||
// across most desktop apps.
|
||||
if (focused_) {
|
||||
std::string_view before(text_.data(), cursor_);
|
||||
float prefixW = static_cast<float>(font_->GetLineWidth(before, devSize));
|
||||
float caretX = originX + prefixW;
|
||||
dl.AddRect({caretX, originY, t, font_->LineHeight(devSize)}, focusBorderColor_);
|
||||
constexpr float kBlinkPeriod = 1.06f;
|
||||
float phase = std::fmod(dl.time, kBlinkPeriod);
|
||||
if (phase < kBlinkPeriod * 0.5f) {
|
||||
std::string_view before(text_.data(), cursor_);
|
||||
float prefixW = static_cast<float>(font_->GetLineWidth(before, devSize));
|
||||
float caretX = originX - scrollX_ + prefixW;
|
||||
dl.AddRect({caretX, originY, t, font_->LineHeight(devSize)}, focusBorderColor_);
|
||||
}
|
||||
}
|
||||
|
||||
dl.PopClip();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue