diff --git a/CMakeLists.txt b/CMakeLists.txt index f463fed..d68eb4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,8 @@ PRIVATE ui/widgets/side_bar_button.h ui/widgets/shadow.cpp ui/widgets/shadow.h + ui/widgets/time_input.cpp + ui/widgets/time_input.h ui/widgets/tooltip.cpp ui/widgets/tooltip.h ui/wrap/fade_wrap.cpp @@ -266,4 +268,6 @@ PUBLIC target_link_libraries(lib_ui PUBLIC desktop-app::lib_base +PRIVATE + desktop-app::external_zlib ) diff --git a/ui/emoji_config.cpp b/ui/emoji_config.cpp index 6680854..148feeb 100644 --- a/ui/emoji_config.cpp +++ b/ui/emoji_config.cpp @@ -35,7 +35,7 @@ constexpr auto kSetVersion = uint32(2); constexpr auto kCacheVersion = uint32(6); constexpr auto kMaxId = uint32(1 << 8); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC constexpr auto kScaleForTouchBar = 150; #endif @@ -80,7 +80,7 @@ auto CanClearUniversal = false; auto WaitingToSwitchBackToId = 0; auto Updates = rpl::event_stream<>(); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC auto TouchbarSize = -1; auto TouchbarInstance = std::unique_ptr(); auto TouchbarEmoji = (Instance*)nullptr; @@ -498,7 +498,7 @@ void Init() { InstanceNormal = std::make_unique(SizeNormal); InstanceLarge = std::make_unique(SizeLarge); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC if (style::Scale() != kScaleForTouchBar) { TouchbarSize = int(style::ConvertScale(18 * 4 / 3., kScaleForTouchBar * style::DevicePixelRatio())); @@ -516,7 +516,7 @@ void Clear() { InstanceNormal = nullptr; InstanceLarge = nullptr; -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC TouchbarInstance = nullptr; TouchbarEmoji = nullptr; #endif @@ -620,7 +620,7 @@ int GetSizeLarge() { return SizeLarge; } -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC int GetSizeTouchbar() { return (style::Scale() == kScaleForTouchBar) ? GetSizeLarge() @@ -763,7 +763,7 @@ const QPixmap &SinglePixmap(EmojiPtr emoji, int fontHeight) { } void Draw(QPainter &p, EmojiPtr emoji, int size, int x, int y) { -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC const auto s = (style::Scale() == kScaleForTouchBar) ? SizeLarge : TouchbarSize; diff --git a/ui/emoji_config.h b/ui/emoji_config.h index 75f10e1..3183258 100644 --- a/ui/emoji_config.h +++ b/ui/emoji_config.h @@ -40,7 +40,7 @@ void ClearNeedSwitchToId(); [[nodiscard]] int GetSizeNormal(); [[nodiscard]] int GetSizeLarge(); -#if defined Q_OS_MAC && !defined OS_MAC_OLD +#ifdef Q_OS_MAC [[nodiscard]] int GetSizeTouchbar(); #endif diff --git a/ui/image/image_prepare.cpp b/ui/image/image_prepare.cpp index aaa4469..8ba2e77 100644 --- a/ui/image/image_prepare.cpp +++ b/ui/image/image_prepare.cpp @@ -10,12 +10,23 @@ #include "ui/style/style_core.h" #include "ui/painter.h" #include "base/flat_map.h" +#include "base/debug_log.h" +#include "base/bytes.h" #include "styles/palette.h" #include "styles/style_basic.h" +#include "zlib.h" +#include +#include +#include +#include + namespace Images { namespace { +// They should be smaller. +constexpr auto kMaxGzipFileSize = 5 * 1024 * 1024; + TG_FORCE_INLINE uint64 blurGetColors(const uchar *p) { return (uint64)p[0] + ((uint64)p[1] << 16) + ((uint64)p[2] << 32) + ((uint64)p[3] << 48); } @@ -68,6 +79,210 @@ std::array PrepareCornersMask(int radius) { return result; } +template // 4 means 16x16, 3 means 8x8 +[[nodiscard]] QImage DitherGeneric(const QImage &image) { + static_assert(kBits >= 1 && kBits <= 4); + + constexpr auto kSquareSide = (1 << kBits); + constexpr auto kShift = kSquareSide / 2; + constexpr auto kMask = (kSquareSide - 1); + + const auto width = image.width(); + const auto height = image.height(); + const auto area = width * height; + const auto shifts = std::make_unique(area); + bytes::set_random(bytes::make_span(shifts.get(), area)); + + // shiftx = int(shift & kMask) - kShift; + // shifty = int((shift >> 4) & kMask) - kShift; + // Clamp shifts close to edges. + for (auto y = 0; y != kShift; ++y) { + const auto min = kShift - y; + const auto shifted = (min << 4); + auto shift = shifts.get() + y * width; + for (const auto till = shift + width; shift != till; ++shift) { + if (((*shift >> 4) & kMask) < min) { + *shift = shifted | (*shift & 0x0F); + } + } + } + for (auto y = height - (kShift - 1); y != height; ++y) { + const auto max = kShift + (height - y - 1); + const auto shifted = (max << 4); + auto shift = shifts.get() + y * width; + for (const auto till = shift + width; shift != till; ++shift) { + if (((*shift >> 4) & kMask) > max) { + *shift = shifted | (*shift & 0x0F); + } + } + } + for (auto shift = shifts.get(), ytill = shift + area + ; shift != ytill + ; shift += width - kShift) { + for (const auto till = shift + kShift; shift != till; ++shift) { + const auto min = (till - shift); + if ((*shift & kMask) < min) { + *shift = (*shift & 0xF0) | min; + } + } + } + for (auto shift = shifts.get(), ytill = shift + area; shift != ytill;) { + shift += width - (kShift - 1); + for (const auto till = shift + (kShift - 1); shift != till; ++shift) { + const auto max = kShift + (till - shift - 1); + if ((*shift & kMask) > max) { + *shift = (*shift & 0xF0) | max; + } + } + } + + auto result = image; + result.detach(); + + const auto src = reinterpret_cast(image.constBits()); + const auto dst = reinterpret_cast(result.bits()); + for (auto index = 0; index != area; ++index) { + const auto shift = shifts[index]; + const auto shiftx = int(shift & kMask) - kShift; + const auto shifty = int((shift >> 4) & kMask) - kShift; + dst[index] = src[index + (shifty * width) + shiftx]; + } + + return result; +} + +[[nodiscard]] QImage GenerateSmallComplexGradient( + const std::vector &colors, + int rotation, + float progress) { + const auto positions = std::vector>{ + { 0.80f, 0.10f }, + { 0.60f, 0.20f }, + { 0.35f, 0.25f }, + { 0.25f, 0.60f }, + { 0.20f, 0.90f }, + { 0.40f, 0.80f }, + { 0.65f, 0.75f }, + { 0.75f, 0.40f }, + }; + const auto positionsForPhase = [&](int phase) { + auto result = std::vector>(4); + for (auto i = 0; i != 4; ++i) { + result[i] = positions[(phase + i * 2) % 8]; + result[i].second = 1.f - result[i].second; + } + return result; + }; + const auto phase = std::clamp(rotation, 0, 315) / 45; + const auto previousPhase = (phase + 1) % 8; + const auto previous = positionsForPhase(previousPhase); + const auto current = positionsForPhase(phase); + + constexpr auto kWidth = 64; + constexpr auto kHeight = 64; + static const auto pixelCache = [&] { + auto result = std::make_unique(kWidth * kHeight * 2); + const auto invwidth = 1.f / kWidth; + const auto invheight = 1.f / kHeight; + auto floats = result.get(); + for (auto y = 0; y != kHeight; ++y) { + const auto directPixelY = y * invheight; + const auto centerDistanceY = directPixelY - 0.5f; + const auto centerDistanceY2 = centerDistanceY * centerDistanceY; + for (auto x = 0; x != kWidth; ++x) { + const auto directPixelX = x * invwidth; + const auto centerDistanceX = directPixelX - 0.5f; + const auto centerDistance = sqrtf( + centerDistanceX * centerDistanceX + centerDistanceY2); + + const auto swirlFactor = 0.35f * centerDistance; + const auto theta = swirlFactor * swirlFactor * 0.8f * 8.0f; + const auto sinTheta = sinf(theta); + const auto cosTheta = cosf(theta); + *floats++ = std::max( + 0.0f, + std::min( + 1.0f, + (0.5f + + centerDistanceX * cosTheta + - centerDistanceY * sinTheta))); + *floats++ = std::max( + 0.0f, + std::min( + 1.0f, + (0.5f + + centerDistanceX * sinTheta + + centerDistanceY * cosTheta))); + } + } + return result; + }(); + const auto colorsCount = int(colors.size()); + auto colorsFloat = std::vector>(colorsCount); + for (auto i = 0; i != colorsCount; ++i) { + colorsFloat[i] = { + float(colors[i].red()), + float(colors[i].green()), + float(colors[i].blue()), + }; + } + auto result = QImage( + kWidth, + kHeight, + QImage::Format_RGB32); + Assert(result.bytesPerLine() == kWidth * 4); + + auto cache = pixelCache.get(); + auto pixels = reinterpret_cast(result.bits()); + for (auto y = 0; y != kHeight; ++y) { + for (auto x = 0; x != kWidth; ++x) { + const auto pixelX = *cache++; + const auto pixelY = *cache++; + + auto distanceSum = 0.f; + auto r = 0.f; + auto g = 0.f; + auto b = 0.f; + for (auto i = 0; i != colorsCount; ++i) { + const auto colorX = previous[i].first + + (current[i].first - previous[i].first) * progress; + const auto colorY = previous[i].second + + (current[i].second - previous[i].second) * progress; + + const auto dx = pixelX - colorX; + const auto dy = pixelY - colorY; + const auto distance = std::max( + 0.0f, + 0.9f - sqrtf(dx * dx + dy * dy)); + const auto square = distance * distance; + const auto fourth = square * square; + distanceSum += fourth; + + r += fourth * colorsFloat[i][0]; + g += fourth * colorsFloat[i][1]; + b += fourth * colorsFloat[i][2]; + } + + const auto red = uint32(r / distanceSum); + const auto green = uint32(g / distanceSum); + const auto blue = uint32(b / distanceSum); + *pixels++ = 0xFF000000U | (red << 16) | (green << 8) | blue; + } + } + return result; +} + +[[nodiscard]] QImage GenerateComplexGradient( + QSize size, + const std::vector &colors, + int rotation, + float progress) { + auto exact = GenerateSmallComplexGradient(colors, rotation, progress); + return (exact.size() == size) + ? exact + : exact.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +} + } // namespace QPixmap PixmapFast(QImage &&image) { @@ -111,6 +326,134 @@ std::array PrepareCorners( return result; } +[[nodiscard]] QByteArray UnpackGzip(const QByteArray &bytes) { + z_stream stream; + stream.zalloc = nullptr; + stream.zfree = nullptr; + stream.opaque = nullptr; + stream.avail_in = 0; + stream.next_in = nullptr; + int res = inflateInit2(&stream, 16 + MAX_WBITS); + if (res != Z_OK) { + return bytes; + } + const auto guard = gsl::finally([&] { inflateEnd(&stream); }); + + auto result = QByteArray(kMaxGzipFileSize + 1, char(0)); + stream.avail_in = bytes.size(); + stream.next_in = reinterpret_cast(const_cast(bytes.data())); + stream.avail_out = 0; + while (!stream.avail_out) { + stream.avail_out = result.size(); + stream.next_out = reinterpret_cast(result.data()); + int res = inflate(&stream, Z_NO_FLUSH); + if (res != Z_OK && res != Z_STREAM_END) { + return bytes; + } else if (!stream.avail_out) { + return bytes; + } + } + result.resize(result.size() - stream.avail_out); + return result; +} + +[[nodiscard]] ReadResult ReadGzipSvg(const ReadArgs &args) { + const auto bytes = UnpackGzip(args.content); + if (bytes.isEmpty()) { + LOG(("Svg Error: Couldn't unpack gzip-ed content.")); + return {}; + } + auto renderer = QSvgRenderer(bytes); + if (!renderer.isValid()) { + LOG(("Svg Error: Invalid data.")); + return {}; + } + auto size = renderer.defaultSize(); + if (!args.maxSize.isEmpty() + && (size.width() > args.maxSize.width() + || size.height() > args.maxSize.height())) { + size = size.scaled(args.maxSize, Qt::KeepAspectRatio); + } + if (size.isEmpty()) { + LOG(("Svg Error: Bad size %1x%2." + ).arg(renderer.defaultSize().width() + ).arg(renderer.defaultSize().height())); + return {}; + } + auto result = ReadResult(); + result.image = QImage(size, QImage::Format_ARGB32_Premultiplied); + result.image.fill(Qt::transparent); + { + QPainter p(&result.image); + renderer.render(&p, QRect(QPoint(), size)); + } + result.format = "svg"; + return result; +} + +[[nodiscard]] ReadResult ReadOther(const ReadArgs &args) { + auto bytes = args.content; + if (bytes.isEmpty()) { + return {}; + } + auto buffer = QBuffer(&bytes); + auto reader = QImageReader(&buffer); + reader.setAutoTransform(true); + if (!reader.canRead()) { + return {}; + } + const auto size = reader.size(); + if (size.width() * size.height() > kReadMaxArea) { + return {}; + } + auto result = ReadResult(); + if (!reader.read(&result.image) || result.image.isNull()) { + return {}; + } + result.animated = reader.supportsAnimation() + && (reader.imageCount() > 1); + result.format = reader.format().toLower(); + return result; +} + +ReadResult Read(ReadArgs &&args) { + if (args.content.isEmpty()) { + if (args.path.isEmpty()) { + return {}; + } + auto file = QFile(args.path); + if (file.size() > kReadBytesLimit + || !file.open(QIODevice::ReadOnly)) { + return {}; + } + args.content = file.readAll(); + } + auto result = args.gzipSvg ? ReadGzipSvg(args) : ReadOther(args); + if (result.image.isNull()) { + args = ReadArgs(); + return {}; + } + if (args.returnContent) { + result.content = args.content; + } else { + args.content = QByteArray(); + } + if (!args.maxSize.isEmpty() + && (result.image.width() > args.maxSize.width() + || result.image.height() > args.maxSize.height())) { + result.image = result.image.scaled( + args.maxSize, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + } + if (args.forceOpaque + && result.format != qstr("jpg") + && result.format != qstr("jpeg")) { + result.image = prepareOpaque(std::move(result.image)); + } + return result; +} + QImage prepareBlur(QImage img) { if (img.isNull()) { return img; @@ -447,6 +790,72 @@ QImage BlurLargeImage(QImage image, int radius) { return image; } +[[nodiscard]] QImage DitherImage(QImage image) { + Expects(image.bytesPerLine() == image.width() * 4); + + const auto width = image.width(); + const auto height = image.height(); + const auto min = std::min(width, height); + const auto max = std::max(width, height); + if (max >= 1024 && min >= 512) { + return DitherGeneric<4>(image); + } else if (max >= 512 && min >= 256) { + return DitherGeneric<3>(image); + } else if (max >= 256 && min >= 128) { + return DitherGeneric<2>(image); + } else if (min >= 32) { + return DitherGeneric<1>(image); + } + return image; +} + +[[nodiscard]] QImage GenerateGradient( + QSize size, + const std::vector &colors, + int rotation, + float progress) { + Expects(!colors.empty()); + Expects(colors.size() <= 4); + + if (size.isEmpty()) { + return QImage(); + } else if (colors.size() > 2) { + return GenerateComplexGradient(size, colors, rotation, progress); + } + auto result = QImage(size, QImage::Format_RGB32); + if (colors.size() == 1) { + result.fill(colors.front()); + return result; + } + + auto p = QPainter(&result); + const auto width = size.width(); + const auto height = size.height(); + const auto [start, finalStop] = [&]() -> std::pair { + const auto type = std::clamp(rotation, 0, 315) / 45; + switch (type) { + case 0: return { { 0, 0 }, { 0, height } }; + case 1: return { { width, 0 }, { 0, height } }; + case 2: return { { width, 0 }, { 0, 0 } }; + case 3: return { { width, height }, { 0, 0 } }; + case 4: return { { 0, height }, { 0, 0 } }; + case 5: return { { 0, height }, { width, 0 } }; + case 6: return { { 0, 0 }, { width, 0 } }; + case 7: return { { 0, 0 }, { width, height } }; + } + Unexpected("Rotation value in GenerateDitheredGradient."); + }(); + auto gradient = QLinearGradient(start, finalStop); + gradient.setStops(QGradientStops{ + { 0.0, colors[0] }, + { 1.0, colors[1] } + }); + p.fillRect(QRect(QPoint(), size), QBrush(std::move(gradient))); + p.end(); + + return result; +} + void prepareCircle(QImage &img) { Assert(!img.isNull()); diff --git a/ui/image/image_prepare.h b/ui/image/image_prepare.h index 60261b7..e697e04 100644 --- a/ui/image/image_prepare.h +++ b/ui/image/image_prepare.h @@ -27,6 +27,13 @@ namespace Images { [[nodiscard]] QPixmap PixmapFast(QImage &&image); [[nodiscard]] QImage BlurLargeImage(QImage image, int radius); +[[nodiscard]] QImage DitherImage(QImage image); + +[[nodiscard]] QImage GenerateGradient( + QSize size, + const std::vector &colors, // colors.size() <= 4. + int rotation = 0, + float progress = 1.f); [[nodiscard]] const std::array &CornersMask( ImageRoundRadius radius); @@ -39,6 +46,28 @@ namespace Images { int radius, const style::color &color); +[[nodiscard]] QByteArray UnpackGzip(const QByteArray &bytes); + +// Try to read images up to 64MB. +inline constexpr auto kReadBytesLimit = 64 * 1024 * 1024; +inline constexpr auto kReadMaxArea = 12'032 * 9'024; + +struct ReadArgs { + QString path; + QByteArray content; + QSize maxSize; + bool gzipSvg = false; + bool forceOpaque = false; + bool returnContent = false; +}; +struct ReadResult { + QImage image; + QByteArray content; + QByteArray format; + bool animated = false; +}; +[[nodiscard]] ReadResult Read(ReadArgs &&args); + QImage prepareBlur(QImage image); void prepareRound( QImage &image, diff --git a/ui/platform/win/ui_window_win.cpp b/ui/platform/win/ui_window_win.cpp index 2c72ddb..130d63e 100644 --- a/ui/platform/win/ui_window_win.cpp +++ b/ui/platform/win/ui_window_win.cpp @@ -10,6 +10,7 @@ #include "ui/platform/win/ui_window_title_win.h" #include "base/platform/base_platform_info.h" #include "base/platform/win/base_windows_safe_library.h" +#include "base/integration.h" #include "base/debug_log.h" #include "styles/palette.h" @@ -150,15 +151,19 @@ bool WindowHelper::NativeFilter::nativeEventFilter( const QByteArray &eventType, void *message, long *result) { + auto filtered = false; const auto msg = static_cast(message); const auto i = _windowByHandle.find(msg->hwnd); - return (i != end(_windowByHandle)) - ? i->second->handleNativeEvent( - msg->message, - msg->wParam, - msg->lParam, - reinterpret_cast(result)) - : false; + if (i != end(_windowByHandle)) { + base::Integration::Instance().enterFromEventLoop([&] { + filtered = i->second->handleNativeEvent( + msg->message, + msg->wParam, + msg->lParam, + reinterpret_cast(result)); + }); + } + return filtered; } WindowHelper::WindowHelper(not_null window) diff --git a/ui/style/style_core.cpp b/ui/style/style_core.cpp index 9914e50..48d1dd4 100644 --- a/ui/style/style_core.cpp +++ b/ui/style/style_core.cpp @@ -144,11 +144,7 @@ namespace internal { QImage createCircleMask(int size, QColor bg, QColor fg) { int realSize = size * DevicePixelRatio(); -#ifndef OS_MAC_OLD auto result = QImage(realSize, realSize, QImage::Format::Format_Grayscale8); -#else // OS_MAC_OLD - auto result = QImage(realSize, realSize, QImage::Format::Format_RGB32); -#endif // OS_MAC_OLD { QPainter p(&result); PainterHighQualityEnabler hq(p); diff --git a/ui/style/style_core_icon.cpp b/ui/style/style_core_icon.cpp index c250beb..09b7c21 100644 --- a/ui/style/style_core_icon.cpp +++ b/ui/style/style_core_icon.cpp @@ -8,6 +8,7 @@ #include "ui/style/style_core.h" #include "base/basic_types.h" +#include "styles/palette.h" #include @@ -20,8 +21,8 @@ uint32 colorKey(QColor c) { } base::flat_map iconMasks; -QMap, QPixmap> iconPixmaps; -OrderedSet iconData; +base::flat_map, QPixmap> iconPixmaps; +base::flat_set iconData; QImage createIconMask(const IconMask *mask, int scale) { auto maskImage = QImage::fromData(mask->data(), mask->size(), "PNG"); @@ -86,6 +87,14 @@ QSize readGeneratedSize(const IconMask *mask, int scale) { } // namespace +MonoIcon::MonoIcon(const MonoIcon &other, const style::palette &palette) +: _mask(other._mask) +, _color( + palette.colorAtIndex( + style::main_palette::indexOfColor(other._color))) +, _offset(other._offset) { +} + MonoIcon::MonoIcon(const IconMask *mask, Color color, QPoint offset) : _mask(mask) , _color(std::move(color)) @@ -268,17 +277,31 @@ void MonoIcon::ensureColorizedImage(QColor color) const { void MonoIcon::createCachedPixmap() const { auto key = qMakePair(_mask, colorKey(_color->c)); - auto j = iconPixmaps.constFind(key); - if (j == iconPixmaps.cend()) { + auto j = iconPixmaps.find(key); + if (j == end(iconPixmaps)) { auto image = colorizeImage(_maskImage, _color); - j = iconPixmaps.insert(key, QPixmap::fromImage(std::move(image))); + j = iconPixmaps.emplace( + key, + QPixmap::fromImage(std::move(image))).first; } - _pixmap = j.value(); + _pixmap = j->second; _size = _pixmap.size() / DevicePixelRatio(); } +IconData::IconData(const IconData &other, const style::palette &palette) { + created(); + _parts.reserve(other._parts.size()); + for (const auto &part : other._parts) { + _parts.push_back(MonoIcon(part, palette)); + } +} + void IconData::created() { - iconData.insert(this); + iconData.emplace(this); +} + +IconData::~IconData() { + iconData.remove(this); } void IconData::fill(QPainter &p, const QRect &rect) const { @@ -304,7 +327,8 @@ void IconData::fill(QPainter &p, const QRect &rect, QColor colorOverride) const } QImage IconData::instance(QColor colorOverride, int scale) const { - Assert(_parts.size() == 1); + Expects(_parts.size() == 1); + auto &part = _parts[0]; Assert(part.offset() == QPoint(0, 0)); return part.instance(colorOverride, scale); @@ -330,6 +354,15 @@ int IconData::height() const { return _height; } +Icon Icon::withPalette(const style::palette &palette) const { + Expects(_data != nullptr); + + auto result = Icon(Qt::Uninitialized); + result._data = new IconData(*_data, palette); + result._owner = true; + return result; +} + void resetIcons() { iconPixmaps.clear(); for (const auto data : iconData) { diff --git a/ui/style/style_core_icon.h b/ui/style/style_core_icon.h index f5c3f59..a9707ca 100644 --- a/ui/style/style_core_icon.h +++ b/ui/style/style_core_icon.h @@ -43,6 +43,7 @@ public: MonoIcon &operator=(const MonoIcon &other) = delete; MonoIcon(MonoIcon &&other) = default; MonoIcon &operator=(MonoIcon &&other) = default; + MonoIcon(const MonoIcon &other, const style::palette &palette); MonoIcon(const IconMask *mask, Color color, QPoint offset); void reset() const; @@ -82,13 +83,18 @@ private: class IconData { public: + struct FromIcons { + }; template - IconData(MonoIcons &&...icons) { + IconData(FromIcons, MonoIcons &&...icons) { created(); _parts.reserve(sizeof...(MonoIcons)); addIcons(std::forward(icons)...); } + IconData(const IconData &other, const style::palette &palette); + ~IconData(); + void reset() { for (const auto &part : _parts) { part.reset(); @@ -149,11 +155,17 @@ public: } template - Icon(MonoIcons&&... icons) : _data(new IconData(std::forward(icons)...)), _owner(true) { + Icon(MonoIcons&&... icons) + : _data(new IconData( + IconData::FromIcons{}, + std::forward(icons)...)) + , _owner(true) { } Icon(const Icon &other) : _data(other._data) { } - Icon(Icon &&other) : _data(base::take(other._data)), _owner(base::take(other._owner)) { + Icon(Icon &&other) + : _data(base::take(other._data)) + , _owner(base::take(other._owner)) { } Icon &operator=(const Icon &other) { Expects(!_owner); @@ -247,6 +259,8 @@ public: return Proxy(*this, paletteOverride); } + Icon withPalette(const style::palette &palette) const; + ~Icon() { if (auto data = base::take(_data)) { if (_owner) { diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 9ed1c80..5c9059f 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -134,14 +134,7 @@ bool IsBad(QChar ch) { || (ch >= 127 && ch < 160 && ch != 156) // qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551 - || (Platform::IsMac() && ch == 6158) - - // tmp hack see https://bugreports.qt.io/browse/QTBUG-48910 - || (Platform::IsMac10_11OrGreater() - && !Platform::IsMac10_12OrGreater() - && ch >= 0x0B00 - && ch <= 0x0B7F - && IsDiac(ch)); + || (Platform::IsMac() && ch == 6158); } } // namespace @@ -1984,12 +1977,7 @@ private: if (item == -1) return; -#ifdef OS_MAC_OLD - auto end = _e->findItem(line.from + line.length - 1); -#else // OS_MAC_OLD auto end = _e->findItem(line.from + line.length - 1, item); -#endif // OS_MAC_OLD - auto blockIndex = _lineStartBlock; auto currentBlock = _t->_blocks[blockIndex].get(); auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr; diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index b535f4e..cd4453e 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -88,9 +88,7 @@ QRegularExpression CreateRegExp(const QString &expression) { auto result = QRegularExpression( expression, QRegularExpression::UseUnicodePropertiesOption); -#ifndef OS_MAC_OLD result.optimize(); -#endif // OS_MAC_OLD return result; } diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 6f27241..74d14d7 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -24,6 +24,7 @@ enum class EntityType : uchar { Mention, MentionName, BotCommand, + MediaTimestamp, Bold, Semibold, diff --git a/ui/ui_utility.cpp b/ui/ui_utility.cpp index f37e607..a2e55e6 100644 --- a/ui/ui_utility.cpp +++ b/ui/ui_utility.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -169,9 +170,7 @@ void SendSynteticMouseEvent(QWidget *widget, QEvent::Type type, Qt::MouseButton , button , QGuiApplication::mouseButtons() | button , QGuiApplication::keyboardModifiers() -#ifndef OS_MAC_OLD , Qt::MouseEventSynthesizedByApplication -#endif // OS_MAC_OLD ); ev.setTimestamp(crl::now()); QGuiApplication::sendEvent(windowHandle, &ev); @@ -220,4 +219,15 @@ void DisableCustomScaling() { } } +int WheelDirection(not_null e) { + // Only a mouse wheel is accepted. + constexpr auto step = static_cast(QWheelEvent::DefaultDeltasPerStep); + const auto delta = e->angleDelta().y(); + const auto absDelta = std::abs(delta); + if (absDelta != step) { + return 0; + } + return (delta / absDelta); +} + } // namespace Ui diff --git a/ui/ui_utility.h b/ui/ui_utility.h index ece602d..f7f6fb1 100644 --- a/ui/ui_utility.h +++ b/ui/ui_utility.h @@ -14,6 +14,7 @@ class QPixmap; class QImage; +class QWheelEvent; enum class RectPart; using RectParts = base::flags; @@ -193,4 +194,6 @@ QPointer MakeWeak(not_null object) { void DisableCustomScaling(); +int WheelDirection(not_null e); + } // namespace Ui diff --git a/ui/widgets/input_fields.cpp b/ui/widgets/input_fields.cpp index 256fbe2..c097995 100644 --- a/ui/widgets/input_fields.cpp +++ b/ui/widgets/input_fields.cpp @@ -1309,7 +1309,6 @@ InputField::InputField( , _lastTextWithTags(value) , _placeholderFull(std::move(placeholder)) { _inner->setDocument(CreateChild(_inner.get(), _st)); - _inner->setAcceptRichText(false); resize(_st.width, _minHeight); @@ -3008,7 +3007,7 @@ void InputField::inputMethodEventInner(QInputMethodEvent *e) { const auto weak = Ui::MakeWeak(this); _inner->QTextEdit::inputMethodEvent(e); - if (weak) { + if (weak && _inputMethodCommit.has_value()) { const auto text = *base::take(_inputMethodCommit); if (!processMarkdownReplaces(text)) { processInstantReplaces(text); diff --git a/ui/widgets/time_input.cpp b/ui/widgets/time_input.cpp new file mode 100644 index 0000000..ee80060 --- /dev/null +++ b/ui/widgets/time_input.cpp @@ -0,0 +1,473 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/widgets/time_input.h" + +#include "ui/widgets/input_fields.h" +#include "ui/ui_utility.h" + +#include +#include + +namespace Ui { +namespace { + +QTime ValidateTime(const QString &value) { + const auto match = QRegularExpression( + "^(\\d{1,2})\\:(\\d\\d)$").match(value); + if (!match.hasMatch()) { + return QTime(); + } + const auto readInt = [](const QString &value) { + auto ref = value.midRef(0); + while (!ref.isEmpty() && ref.at(0) == '0') { + ref = ref.mid(1); + } + return ref.toInt(); + }; + return QTime(readInt(match.captured(1)), readInt(match.captured(2))); +} + +QString GetHour(const QString &value) { + if (const auto time = ValidateTime(value); time.isValid()) { + return QString::number(time.hour()); + } + return QString(); +} + +QString GetMinute(const QString &value) { + if (const auto time = ValidateTime(value); time.isValid()) { + return QString("%1").arg(time.minute(), 2, 10, QChar('0')); + } + return QString(); +} + +} // namespace + +class TimePart final : public MaskedInputField { +public: + using MaskedInputField::MaskedInputField; + + void setMaxValue(int value); + void setWheelStep(int value); + + rpl::producer<> erasePrevious() const; + rpl::producer putNext() const; + +protected: + void keyPressEvent(QKeyEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + + void correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) override; + +private: + int _maxValue = 0; + int _maxDigits = 0; + int _wheelStep = 0; + rpl::event_stream<> _erasePrevious; + rpl::event_stream _putNext; + +}; + +int Number(not_null field) { + const auto text = field->getLastText(); + auto ref = text.midRef(0); + while (!ref.isEmpty() && ref.at(0) == '0') { + ref = ref.mid(1); + } + return ref.toInt(); +} + +void TimePart::setMaxValue(int value) { + _maxValue = value; + _maxDigits = 0; + while (value > 0) { + ++_maxDigits; + value /= 10; + } +} + +void TimePart::setWheelStep(int value) { + _wheelStep = value; +} + +rpl::producer<> TimePart::erasePrevious() const { + return _erasePrevious.events(); +} + +rpl::producer TimePart::putNext() const { + return _putNext.events(); +} + +void TimePart::keyPressEvent(QKeyEvent *e) { + const auto isBackspace = (e->key() == Qt::Key_Backspace); + const auto isBeginning = (cursorPosition() == 0); + if (isBackspace && isBeginning && !hasSelectedText()) { + _erasePrevious.fire({}); + } else { + MaskedInputField::keyPressEvent(e); + } +} + +void TimePart::wheelEvent(QWheelEvent *e) { + const auto direction = WheelDirection(e); + auto time = Number(this) + (direction * _wheelStep); + const auto max = _maxValue + 1; + if (time < 0) { + time += max; + } else if (time >= max) { + time -= max; + } + setText(QString::number(time)); +} + +void TimePart::correctValue( + const QString &was, + int wasCursor, + QString &now, + int &nowCursor) { + auto newText = QString(); + auto newCursor = -1; + const auto oldCursor = nowCursor; + const auto oldLength = now.size(); + auto accumulated = 0; + auto limit = 0; + for (; limit != oldLength; ++limit) { + if (now[limit].isDigit()) { + accumulated *= 10; + accumulated += (now[limit].unicode() - '0'); + if (accumulated > _maxValue || limit == _maxDigits) { + break; + } + } + } + for (auto i = 0; i != limit;) { + if (now[i].isDigit()) { + newText += now[i]; + } + if (++i == oldCursor) { + newCursor = newText.size(); + } + } + if (newCursor < 0) { + newCursor = newText.size(); + } + if (newText != now) { + now = newText; + setText(now); + startPlaceholderAnimation(); + } + if (newCursor != nowCursor) { + nowCursor = newCursor; + setCursorPosition(nowCursor); + } + if (accumulated > _maxValue + || (limit == _maxDigits && oldLength > _maxDigits)) { + if (oldCursor > limit) { + _putNext.fire('0' + (accumulated % 10)); + } else { + _putNext.fire(0); + } + } +} + +TimeInput::TimeInput( + QWidget *parent, + const QString &value, + const style::InputField &stField, + const style::InputField &stDateField, + const style::FlatLabel &stSeparator, + const style::margins &stSeparatorPadding) +: RpWidget(parent) +, _stField(stField) +, _stDateField(stDateField) +, _stSeparator(stSeparator) +, _stSeparatorPadding(stSeparatorPadding) +, _hour( + this, + _stField, + rpl::never(), + GetHour(value)) +, _separator1( + this, + object_ptr( + this, + QString(":"), + _stSeparator), + _stSeparatorPadding) +, _minute( + this, + _stField, + rpl::never(), + GetMinute(value)) +, _value(valueCurrent()) { + const auto focused = [=](const object_ptr &field) { + return [this, pointer = MakeWeak(field.data())]{ + _borderAnimationStart = pointer->borderAnimationStart() + + pointer->x() + - _hour->x(); + setFocused(true); + _focuses.fire({}); + }; + }; + const auto blurred = [=] { + setFocused(false); + }; + const auto changed = [=] { + _value = valueCurrent(); + }; + connect(_hour, &MaskedInputField::focused, focused(_hour)); + connect(_minute, &MaskedInputField::focused, focused(_minute)); + connect(_hour, &MaskedInputField::blurred, blurred); + connect(_minute, &MaskedInputField::blurred, blurred); + connect(_hour, &MaskedInputField::changed, changed); + connect(_minute, &MaskedInputField::changed, changed); + _hour->setMaxValue(23); + _hour->setWheelStep(1); + _hour->putNext() | rpl::start_with_next([=](QChar ch) { + putNext(_minute, ch); + }, lifetime()); + _minute->setMaxValue(59); + _minute->setWheelStep(10); + _minute->erasePrevious() | rpl::start_with_next([=] { + erasePrevious(_hour); + }, lifetime()); + _separator1->setAttribute(Qt::WA_TransparentForMouseEvents); + setMouseTracking(true); + + _value.changes( + ) | rpl::start_with_next([=] { + setErrorShown(false); + }, lifetime()); + + const auto submitHour = [=] { + if (hour()) { + _minute->setFocus(); + } + }; + const auto submitMinute = [=] { + if (minute()) { + if (hour()) { + _submitRequests.fire({}); + } else { + _hour->setFocus(); + } + } + }; + connect( + _hour, + &MaskedInputField::submitted, + submitHour); + connect( + _minute, + &MaskedInputField::submitted, + submitMinute); +} + +void TimeInput::putNext(const object_ptr &field, QChar ch) { + field->setCursorPosition(0); + if (ch.unicode()) { + field->setText(ch + field->getLastText()); + field->setCursorPosition(1); + } + field->setFocus(); +} + +void TimeInput::erasePrevious(const object_ptr &field) { + const auto text = field->getLastText(); + if (!text.isEmpty()) { + field->setCursorPosition(text.size() - 1); + field->setText(text.mid(0, text.size() - 1)); + } + field->setFocus(); +} + +bool TimeInput::setFocusFast() { + if (hour()) { + _minute->setFocusFast(); + } else { + _hour->setFocusFast(); + } + return true; +} + +int TimeInput::hour() const { + return Number(_hour); +} + +int TimeInput::minute() const { + return Number(_minute); +} + +QString TimeInput::valueCurrent() const { + const auto result = QString("%1:%2" + ).arg(hour() + ).arg(minute(), 2, 10, QChar('0')); + return ValidateTime(result).isValid() ? result : QString(); +} + +rpl::producer TimeInput::value() const { + return _value.value(); +} + +rpl::producer<> TimeInput::submitRequests() const { + return _submitRequests.events(); +} + +rpl::producer<> TimeInput::focuses() const { + return _focuses.events(); +} + +void TimeInput::paintEvent(QPaintEvent *e) { + Painter p(this); + + const auto &_st = _stDateField; + const auto height = _st.heightMin; + if (_st.border) { + p.fillRect(0, height - _st.border, width(), _st.border, _st.borderFg); + } + auto errorDegree = _a_error.value(_error ? 1. : 0.); + auto borderShownDegree = _a_borderShown.value(1.); + auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.); + if (_st.borderActive && (borderOpacity > 0.)) { + auto borderStart = std::clamp(_borderAnimationStart, 0, width()); + auto borderFrom = qRound(borderStart * (1. - borderShownDegree)); + auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree); + if (borderTo > borderFrom) { + auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree); + p.setOpacity(borderOpacity); + p.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg); + p.setOpacity(1); + } + } +} + +template +bool TimeInput::insideSeparator(QPoint position, const Widget &widget) const { + const auto x = position.x(); + const auto y = position.y(); + return (x >= widget->x() && x < widget->x() + widget->width()) + && (y >= _hour->y() && y < _hour->y() + _hour->height()); +} + +void TimeInput::mouseMoveEvent(QMouseEvent *e) { + const auto cursor = insideSeparator(e->pos(), _separator1) + ? style::cur_text + : style::cur_default; + if (_cursor != cursor) { + _cursor = cursor; + setCursor(_cursor); + } +} + +void TimeInput::mousePressEvent(QMouseEvent *e) { + const auto x = e->pos().x(); + const auto focus1 = [&] { + if (_hour->getLastText().size() > 1) { + _minute->setFocus(); + } else { + _hour->setFocus(); + } + }; + if (insideSeparator(e->pos(), _separator1)) { + focus1(); + _borderAnimationStart = x - _hour->x(); + } +} + +int TimeInput::resizeGetHeight(int width) { + const auto &_st = _stField; + const auto &font = _st.placeholderFont; + const auto addToWidth = _stSeparatorPadding.left(); + const auto hourWidth = _st.textMargins.left() + + _st.placeholderMargins.left() + + font->width(QString("23")) + + _st.placeholderMargins.right() + + _st.textMargins.right() + + addToWidth; + const auto minuteWidth = _st.textMargins.left() + + _st.placeholderMargins.left() + + font->width(QString("59")) + + _st.placeholderMargins.right() + + _st.textMargins.right() + + addToWidth; + const auto full = hourWidth + - addToWidth + + _separator1->width() + + minuteWidth + - addToWidth; + auto left = (width - full) / 2; + auto top = 0; + _hour->setGeometry(left, top, hourWidth, _hour->height()); + left += hourWidth - addToWidth; + _separator1->resizeToNaturalWidth(width); + _separator1->move(left, top); + left += _separator1->width(); + _minute->setGeometry(left, top, minuteWidth, _minute->height()); + return _stDateField.heightMin; +} + +void TimeInput::showError() { + setErrorShown(true); + if (!_focused) { + setInnerFocus(); + } +} + +void TimeInput::setInnerFocus() { + if (hour()) { + _minute->setFocus(); + } else { + _hour->setFocus(); + } +} + +void TimeInput::setErrorShown(bool error) { + if (_error != error) { + _error = error; + _a_error.start( + [=] { update(); }, + _error ? 0. : 1., + _error ? 1. : 0., + _stDateField.duration); + startBorderAnimation(); + } +} + +void TimeInput::setFocused(bool focused) { + if (_focused != focused) { + _focused = focused; + _a_focused.start( + [=] { update(); }, + _focused ? 0. : 1., + _focused ? 1. : 0., + _stDateField.duration); + startBorderAnimation(); + } +} + +void TimeInput::startBorderAnimation() { + auto borderVisible = (_error || _focused); + if (_borderVisible != borderVisible) { + _borderVisible = borderVisible; + const auto duration = _stDateField.duration; + if (_borderVisible) { + if (_a_borderOpacity.animating()) { + _a_borderOpacity.start([=] { update(); }, 0., 1., duration); + } else { + _a_borderShown.start([=] { update(); }, 0., 1., duration); + } + } else { + _a_borderOpacity.start([=] { update(); }, 1., 0., duration); + } + } +} + +} // namespace Ui diff --git a/ui/widgets/time_input.h b/ui/widgets/time_input.h new file mode 100644 index 0000000..bc8a187 --- /dev/null +++ b/ui/widgets/time_input.h @@ -0,0 +1,80 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "ui/rp_widget.h" + +#include "ui/effects/animations.h" +#include "ui/widgets/labels.h" + +namespace Ui { + +class TimePart; + +class TimeInput final : public RpWidget { +public: + TimeInput( + QWidget *parent, + const QString &value, + const style::InputField &stField, + const style::InputField &stDateField, + const style::FlatLabel &stSeparator, + const style::margins &stSeparatorPadding); + + bool setFocusFast(); + rpl::producer value() const; + rpl::producer<> submitRequests() const; + rpl::producer<> focuses() const; + QString valueCurrent() const; + void showError(); + + int resizeGetHeight(int width) override; + +protected: + void paintEvent(QPaintEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + +private: + void setInnerFocus(); + void putNext(const object_ptr &field, QChar ch); + void erasePrevious(const object_ptr &field); + void setErrorShown(bool error); + void setFocused(bool focused); + void startBorderAnimation(); + template + bool insideSeparator(QPoint position, const Widget &widget) const; + + int hour() const; + int minute() const; + + const style::InputField &_stField; + const style::InputField &_stDateField; + const style::FlatLabel &_stSeparator; + const style::margins &_stSeparatorPadding; + + object_ptr _hour; + object_ptr> _separator1; + object_ptr _minute; + rpl::variable _value; + rpl::event_stream<> _submitRequests; + rpl::event_stream<> _focuses; + + style::cursor _cursor = style::cur_default; + Animations::Simple _a_borderShown; + int _borderAnimationStart = 0; + Animations::Simple _a_borderOpacity; + bool _borderVisible = false; + + Animations::Simple _a_error; + bool _error = false; + Animations::Simple _a_focused; + bool _focused = false; + +}; + +} // namespace Ui diff --git a/ui/widgets/widgets.style b/ui/widgets/widgets.style index 73697ea..aea2549 100644 --- a/ui/widgets/widgets.style +++ b/ui/widgets/widgets.style @@ -1188,6 +1188,12 @@ historySendActionUploadStrokeNumerator: 16px; historySendActionUploadSizeNumerator: 32px; historySendActionUploadDenominator: 8.; +historySendActionChooseStickerDuration: 2000; +historySendActionChooseStickerPosition: point(1px, -10px); +historySendActionChooseStickerEyeWidth: 7px; +historySendActionChooseStickerEyeHeight: 11px; +historySendActionChooseStickerEyeStep: 2px; + MediaPlayerButton { playPosition: point; playOuter: size;