470 lines
18 KiB
C++
470 lines
18 KiB
C++
#include "text-fields.hh"
|
|
|
|
#include "modern-qt/utility/animation/animatable.hh"
|
|
#include "modern-qt/utility/animation/state/pid.hh"
|
|
#include "modern-qt/utility/animation/transition.hh"
|
|
#include "modern-qt/utility/painter/common.hh"
|
|
#include "modern-qt/utility/painter/container.hh"
|
|
#include "modern-qt/utility/painter/helper.hh"
|
|
#include "modern-qt/utility/painter/shape.hh"
|
|
|
|
#include <qpainter.h>
|
|
#include <qpainterpath.h>
|
|
|
|
using namespace creeper::text_field::internal;
|
|
|
|
struct BasicTextField::Impl {
|
|
public:
|
|
explicit Impl(BasicTextField& self) noexcept
|
|
: self { self }
|
|
, animatable { self } {
|
|
|
|
{
|
|
auto state = std::make_shared<PidState<double>>();
|
|
|
|
state->config.kp = 20.0;
|
|
state->config.ki = 00.0;
|
|
state->config.kd = 00.0;
|
|
state->config.epsilon = 1e-2;
|
|
|
|
label_position = make_transition(animatable, std::move(state));
|
|
}
|
|
|
|
self.setAlignment(Qt::AlignVCenter);
|
|
set_measurements(Measurements {});
|
|
}
|
|
|
|
/// @note https://m3.material.io/components/text-fields/specs
|
|
auto set_color_scheme(const ColorScheme& scheme) -> void {
|
|
|
|
color_specs.enabled.container = scheme.surface_container_highest;
|
|
color_specs.enabled.label_text = scheme.on_surface_variant;
|
|
color_specs.enabled.leading_icon = scheme.on_surface_variant;
|
|
color_specs.enabled.trailing_icon = scheme.on_surface_variant;
|
|
color_specs.enabled.active_indicator = scheme.on_surface_variant;
|
|
color_specs.enabled.supporting_text = scheme.on_surface_variant;
|
|
color_specs.enabled.input_text = scheme.on_surface;
|
|
color_specs.enabled.caret = scheme.primary;
|
|
color_specs.enabled.outline = scheme.outline;
|
|
|
|
color_specs.disabled.container = scheme.on_surface;
|
|
color_specs.disabled.container.setAlphaF(0.04);
|
|
color_specs.disabled.label_text = scheme.on_surface;
|
|
color_specs.disabled.label_text.setAlphaF(0.38);
|
|
color_specs.disabled.leading_icon = scheme.on_surface;
|
|
color_specs.disabled.leading_icon.setAlphaF(0.38);
|
|
color_specs.disabled.trailing_icon = scheme.on_surface;
|
|
color_specs.disabled.trailing_icon.setAlphaF(0.38);
|
|
color_specs.disabled.supporting_text = scheme.on_surface;
|
|
color_specs.disabled.supporting_text.setAlphaF(0.38);
|
|
color_specs.disabled.input_text = scheme.on_surface;
|
|
color_specs.disabled.input_text.setAlphaF(0.38);
|
|
color_specs.disabled.active_indicator = scheme.on_surface;
|
|
color_specs.disabled.active_indicator.setAlphaF(0.38);
|
|
color_specs.disabled.outline = scheme.outline;
|
|
color_specs.disabled.outline.setAlphaF(0.38);
|
|
|
|
color_specs.focused.container = scheme.surface_container_highest;
|
|
color_specs.focused.label_text = scheme.primary;
|
|
color_specs.focused.leading_icon = scheme.on_surface_variant;
|
|
color_specs.focused.trailing_icon = scheme.on_surface_variant;
|
|
color_specs.focused.input_text = scheme.on_surface;
|
|
color_specs.focused.supporting_text = scheme.on_surface_variant;
|
|
color_specs.focused.active_indicator = scheme.primary;
|
|
color_specs.focused.outline = scheme.primary;
|
|
|
|
color_specs.error.container = scheme.surface_container_highest;
|
|
color_specs.error.active_indicator = scheme.error;
|
|
color_specs.error.label_text = scheme.error;
|
|
color_specs.error.input_text = scheme.on_surface;
|
|
color_specs.error.supporting_text = scheme.error;
|
|
color_specs.error.leading_icon = scheme.on_surface_variant;
|
|
color_specs.error.trailing_icon = scheme.error;
|
|
color_specs.error.caret = scheme.error;
|
|
color_specs.error.outline = scheme.error;
|
|
|
|
color_specs.state_layer = scheme.on_surface;
|
|
color_specs.state_layer.setAlphaF(0.08);
|
|
color_specs.selection_container = scheme.primary;
|
|
color_specs.selection_container.setAlphaF(0.38);
|
|
|
|
const auto& color = get_color_tokens();
|
|
sync_basic_text_style( //
|
|
color.input_text, //
|
|
Qt::transparent, //
|
|
color.input_text, //
|
|
color_specs.selection_container //
|
|
);
|
|
}
|
|
|
|
auto load_theme_manager(ThemeManager& manager) -> void {
|
|
manager.append_handler(&self, [this](const ThemeManager& manager) { //
|
|
set_color_scheme(manager.color_scheme());
|
|
});
|
|
}
|
|
|
|
auto set_label_text(const QString& text) -> void { label_text = text; }
|
|
|
|
auto set_leading_icon(const QString& code, const QString& font) -> void {
|
|
leading_icon_code = code;
|
|
leading_font_name = font;
|
|
|
|
is_update_component_status = false;
|
|
use_leading_icon = true;
|
|
}
|
|
|
|
auto set_measurements(const Measurements& measurements) noexcept -> void {
|
|
this->measurements = measurements;
|
|
self.setFixedHeight(measurements.container_height + measurements.standard_font_height);
|
|
|
|
is_update_component_status = false;
|
|
}
|
|
|
|
auto paint_filled(QPaintEvent*) -> void {
|
|
const auto rect = self.rect();
|
|
const auto color = get_color_tokens();
|
|
|
|
constexpr auto container_radius = 5;
|
|
|
|
update_component_status(FieldType::FILLED);
|
|
|
|
auto painter = QPainter { &self };
|
|
// Container
|
|
{
|
|
util::PainterHelper { painter }
|
|
.set_render_hint(QPainter::Antialiasing)
|
|
.rounded_rectangle(color.container, Qt::transparent, 0, rect, container_radius,
|
|
container_radius, 0, 0);
|
|
}
|
|
// Active Indicator
|
|
{
|
|
const auto p0 = rect.bottomLeft();
|
|
const auto p1 = rect.bottomRight();
|
|
painter.setBrush(Qt::NoBrush);
|
|
painter.setPen(QPen { color.active_indicator, filled_line_width() });
|
|
painter.drawLine(p0, p1);
|
|
}
|
|
// Leading icon
|
|
|
|
if (!leading_icon.isNull()) {
|
|
//
|
|
} else if (!leading_icon_code.isEmpty()) {
|
|
|
|
const auto rect = QRectF {
|
|
1.0 * measurements.row_padding_with_icons,
|
|
0.5 * (measurements.container_height - measurements.icon_rect_size),
|
|
1.0 * measurements.icon_rect_size,
|
|
1.0 * measurements.icon_rect_size,
|
|
};
|
|
|
|
painter.save();
|
|
|
|
painter.setBrush(Qt::NoBrush);
|
|
painter.setPen(QPen { color.leading_icon });
|
|
painter.setFont(leading_icon_font);
|
|
painter.drawText(rect, leading_icon_code, { Qt::AlignCenter });
|
|
|
|
painter.restore();
|
|
}
|
|
// Label Text
|
|
if (!label_text.isEmpty()) {
|
|
|
|
const auto position = self.text().isEmpty() ? *label_position : 1.;
|
|
const auto margins = self.textMargins();
|
|
// qDebug() << "margins = " << margins;
|
|
const auto center_label_padding =
|
|
0.5 * (measurements.container_height - measurements.icon_rect_size);
|
|
const auto rect_center = QRectF {
|
|
QPointF(margins.left(), center_label_padding),
|
|
QPointF(self.width() - margins.right(), self.height() - center_label_padding),
|
|
};
|
|
const auto rect_top = QRectF {
|
|
QPointF(margins.left(), measurements.col_padding),
|
|
QPointF(self.width() - margins.right(), margins.top()),
|
|
};
|
|
|
|
const auto rect = animate::interpolate(rect_center, rect_top, position);
|
|
|
|
const auto scale = 1. - position * 0.25;
|
|
|
|
const auto anchor = QPointF { rect.left(), rect.center().y() };
|
|
|
|
painter.save();
|
|
|
|
painter.translate(anchor);
|
|
painter.scale(scale, scale);
|
|
painter.translate(-anchor);
|
|
|
|
painter.setBrush(Qt::NoBrush);
|
|
painter.setPen(QPen { color.label_text });
|
|
painter.setFont(standard_text_font);
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
painter.drawText(rect, label_text, { Qt::AlignVCenter | Qt::AlignLeading });
|
|
|
|
painter.restore();
|
|
}
|
|
// Hovered State Layer
|
|
if (is_hovered) {
|
|
util::PainterHelper { painter }
|
|
.set_render_hint(QPainter::Antialiasing)
|
|
.rounded_rectangle(color_specs.state_layer, Qt::transparent, 0, rect,
|
|
container_radius, container_radius, 0, 0);
|
|
}
|
|
}
|
|
|
|
auto paint_outlined(QPaintEvent*) -> void {
|
|
const auto& measurements = this->measurements;
|
|
const auto& color_tokens = get_color_tokens();
|
|
|
|
update_component_status(FieldType::OUTLINED);
|
|
{
|
|
using namespace painter;
|
|
using namespace painter::common::pro;
|
|
auto painter = qt::painter { &self };
|
|
|
|
/// Cache Calculate
|
|
const auto container_width = self.width();
|
|
const auto container_height = measurements.container_height;
|
|
|
|
const auto input_leading_padding = use_leading_icon
|
|
? measurements.row_padding_with_icons + measurements.icon_rect_size
|
|
+ measurements.padding_icons_text
|
|
: measurements.row_padding_without_icons;
|
|
const auto input_trailing_padding = use_trailing_icon
|
|
? measurements.row_padding_with_icons + measurements.icon_rect_size
|
|
+ measurements.padding_icons_text
|
|
: measurements.row_padding_without_icons;
|
|
const auto input_vertical_padding =
|
|
0.5 * (measurements.container_height - measurements.icon_rect_size);
|
|
|
|
/// Container
|
|
const auto container_size = qt::size(self.width(), container_height);
|
|
const auto container_thickness = is_focused ? 2. : is_hovered ? 1.5 : 1.;
|
|
|
|
/// Leading Icon
|
|
const auto leading_box_size = use_leading_icon
|
|
? qt::size(measurements.icon_rect_size, container_height)
|
|
: qt::size(0, 0);
|
|
|
|
/// Label Text
|
|
const auto position = self.text().isEmpty() ? *label_position : 1.;
|
|
const auto text_scale = animate::interpolate(1., 0.75, position);
|
|
|
|
auto text_option = qt::text_option {};
|
|
text_option.setWrapMode(QTextOption::NoWrap);
|
|
text_option.setAlignment(Qt::AlignLeading | Qt::AlignVCenter);
|
|
|
|
const auto text_width = measure_text(standard_text_font, label_text, text_option);
|
|
|
|
auto label_origin = qt::point {};
|
|
auto label_size = qt::size {};
|
|
{
|
|
const auto begin_y = input_vertical_padding;
|
|
const auto final_y = -0.5 * measurements.standard_font_height;
|
|
|
|
const auto top_padding = use_leading_icon ? measurements.row_padding_with_icons
|
|
: measurements.row_padding_without_icons;
|
|
const auto begin_origin = qt::point(input_leading_padding, begin_y);
|
|
const auto final_origin = qt::point(top_padding, final_y);
|
|
|
|
const auto begin_size = qt::size(text_width, measurements.icon_rect_size);
|
|
const auto final_size =
|
|
qt::size(text_scale * text_width, measurements.standard_font_height);
|
|
|
|
label_origin = animate::interpolate(begin_origin, final_origin, position);
|
|
label_size = animate::interpolate(begin_size, final_size, position);
|
|
}
|
|
const auto label_background_size = qt::size {
|
|
label_size.width() + 2 * measurements.row_padding_populated_label_text,
|
|
label_size.height(),
|
|
};
|
|
|
|
Paint::Box {
|
|
BoxImpl { self.size(), Qt::AlignCenter },
|
|
Paint::Surface {
|
|
SurfaceImpl { container_size },
|
|
Paint::Buffer {
|
|
BufferImpl { container_size },
|
|
Paint::RoundedRectangle {
|
|
Size { container_size },
|
|
Outline { color_tokens.outline, container_thickness },
|
|
Radiuses { 5 },
|
|
},
|
|
Paint::EraseRectangle {
|
|
Origin { label_origin },
|
|
Size { label_background_size },
|
|
},
|
|
},
|
|
Paint::Box {
|
|
BoxImpl { label_background_size, Qt::AlignHCenter, label_origin },
|
|
Paint::Text {
|
|
TextOption { text_option },
|
|
Font { standard_text_font },
|
|
Size { label_background_size },
|
|
Text { label_text },
|
|
Color { color_tokens.label_text },
|
|
Scale { text_scale },
|
|
},
|
|
},
|
|
Paint::Box {
|
|
BoxImpl { leading_box_size, Qt::AlignCenter,
|
|
{ static_cast<qreal>(measurements.row_padding_with_icons), 0 } },
|
|
Paint::Text {
|
|
TextOption { Qt::AlignCenter },
|
|
Size { leading_box_size },
|
|
Font { leading_icon_font },
|
|
Text { leading_icon_code },
|
|
Color { color_tokens.leading_icon },
|
|
},
|
|
},
|
|
},
|
|
}(painter);
|
|
}
|
|
}
|
|
|
|
auto enter_event(qt::EnterEvent*) -> void {
|
|
is_hovered = true;
|
|
self.update();
|
|
}
|
|
|
|
auto leave_event(QEvent*) -> void {
|
|
is_hovered = false;
|
|
self.update();
|
|
}
|
|
|
|
auto focus_in(QFocusEvent*) -> void {
|
|
is_focused = true;
|
|
update_label_position();
|
|
}
|
|
|
|
auto focus_out(QFocusEvent*) -> void {
|
|
is_focused = false;
|
|
update_label_position();
|
|
}
|
|
|
|
private:
|
|
enum class FieldType { FILLED, OUTLINED };
|
|
|
|
auto update_component_status(FieldType type) -> void {
|
|
if (is_update_component_status) return;
|
|
|
|
const auto padding_with_icon = measurements.row_padding_with_icons
|
|
+ measurements.icon_rect_size + measurements.padding_icons_text;
|
|
const auto padding_without_icon = measurements.row_padding_without_icons;
|
|
|
|
const auto left_padding = use_leading_icon ? padding_with_icon : padding_without_icon;
|
|
const auto tail_padding = use_trailing_icon ? padding_with_icon : padding_without_icon;
|
|
|
|
switch (type) {
|
|
case FieldType::FILLED: {
|
|
const auto top_padding = measurements.col_padding + measurements.label_rect_size;
|
|
const auto bot_padding = measurements.col_padding;
|
|
self.setTextMargins(left_padding, top_padding, tail_padding, bot_padding);
|
|
break;
|
|
}
|
|
case FieldType::OUTLINED: {
|
|
const auto input_padding =
|
|
0.5 * (measurements.container_height - measurements.input_rect_size);
|
|
self.setTextMargins(left_padding, input_padding, tail_padding, input_padding);
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto font = self.font();
|
|
font.setPixelSize(measurements.standard_font_height);
|
|
self.setFont(font);
|
|
|
|
standard_text_font = self.font();
|
|
standard_text_font.setPixelSize(measurements.standard_font_height);
|
|
|
|
leading_icon_font = QFont { leading_font_name };
|
|
leading_icon_font.setPointSizeF(
|
|
0.5 * (measurements.standard_font_height + measurements.icon_rect_size));
|
|
|
|
is_update_component_status = true;
|
|
}
|
|
|
|
auto update_label_position() -> void { label_position->transition_to(is_focused ? 1.0 : 0.0); }
|
|
|
|
auto sync_basic_text_style(const QColor& text, const QColor& background,
|
|
const QColor& selection_text, const QColor& selection_background) -> void {
|
|
|
|
constexpr auto to_rgba = [](const QColor& color) {
|
|
return QStringLiteral("rgba(%1, %2, %3, %4)")
|
|
.arg(color.red())
|
|
.arg(color.green())
|
|
.arg(color.blue())
|
|
.arg(color.alpha());
|
|
};
|
|
|
|
constexpr auto kQLineEditStyle = R"(
|
|
QLineEdit {
|
|
border: none;
|
|
color: %1;
|
|
background-color: %2;
|
|
selection-color: %3;
|
|
selection-background-color: %4;
|
|
}
|
|
)";
|
|
|
|
const auto qss = QString { kQLineEditStyle };
|
|
self.setStyleSheet(qss.arg(to_rgba(text))
|
|
.arg(to_rgba(background))
|
|
.arg(to_rgba(selection_text))
|
|
.arg(to_rgba(selection_background)));
|
|
}
|
|
|
|
auto get_color_tokens() const -> ColorSpecs::Tokens const& {
|
|
return is_disable ? color_specs.disabled
|
|
: is_error ? color_specs.error
|
|
: is_focused ? color_specs.focused
|
|
: color_specs.enabled;
|
|
}
|
|
|
|
auto filled_line_width() const -> double {
|
|
constexpr auto normal_width = 1;
|
|
constexpr auto active_width = 3;
|
|
return (is_focused && !is_disable) ? active_width : normal_width;
|
|
}
|
|
|
|
constexpr auto measure_text(const QFont& font, const QString& text, const QTextOption& options)
|
|
-> double {
|
|
const auto fm = QFontMetricsF(font);
|
|
const auto size = fm.size(Qt::TextSingleLine, text);
|
|
return size.width();
|
|
}
|
|
|
|
private:
|
|
Measurements measurements;
|
|
ColorSpecs color_specs;
|
|
|
|
bool is_disable = false;
|
|
bool is_hovered = false;
|
|
bool is_focused = false;
|
|
bool is_error = false;
|
|
bool is_update_component_status = false;
|
|
|
|
QString label_text;
|
|
QString hint_text;
|
|
QString supporting_text;
|
|
|
|
bool use_leading_icon = false;
|
|
bool use_trailing_icon = false;
|
|
|
|
QIcon leading_icon;
|
|
QString leading_icon_code;
|
|
QString leading_font_name;
|
|
|
|
QIcon trailing_icon;
|
|
QString trailing_code;
|
|
QString trailing_font;
|
|
|
|
QFont leading_icon_font;
|
|
QFont standard_text_font;
|
|
|
|
Animatable animatable;
|
|
std::unique_ptr<TransitionValue<PidState<double>>> label_position;
|
|
|
|
BasicTextField& self;
|
|
};
|