зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1666739 - Add an optional opacity threshold for visibility hit-test. r=mconley,miko
This is a best-effort thing of course, but so is the rest of the visibility threshold stuff in practice and this should be good enough. Differential Revision: https://phabricator.services.mozilla.com/D98360
This commit is contained in:
Родитель
296459ef20
Коммит
75be5de2e1
|
@ -12918,8 +12918,8 @@ already_AddRefed<nsDOMCaretPosition> Document::CaretPositionFromPoint(
|
|||
|
||||
nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
|
||||
RelativeTo{rootFrame}, pt,
|
||||
{FrameForPointOption::IgnorePaintSuppression,
|
||||
FrameForPointOption::IgnoreCrossDoc});
|
||||
{{FrameForPointOption::IgnorePaintSuppression,
|
||||
FrameForPointOption::IgnoreCrossDoc}});
|
||||
if (!ptFrame) {
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
@ -332,6 +332,7 @@ Element* DocumentOrShadowRoot::GetFullscreenElement() {
|
|||
namespace {
|
||||
|
||||
using FrameForPointOption = nsLayoutUtils::FrameForPointOption;
|
||||
using FrameForPointOptions = nsLayoutUtils::FrameForPointOptions;
|
||||
|
||||
// Whether only one node or multiple nodes is requested.
|
||||
enum class Multiple {
|
||||
|
@ -360,7 +361,7 @@ nsINode* CastTo<nsINode>(nsIContent* aContent) {
|
|||
|
||||
template <typename NodeOrElement>
|
||||
static void QueryNodesFromRect(DocumentOrShadowRoot& aRoot, const nsRect& aRect,
|
||||
EnumSet<FrameForPointOption> aOptions,
|
||||
FrameForPointOptions aOptions,
|
||||
FlushLayout aShouldFlushLayout,
|
||||
Multiple aMultiple, ViewportType aViewportType,
|
||||
nsTArray<RefPtr<NodeOrElement>>& aNodes) {
|
||||
|
@ -390,8 +391,8 @@ static void QueryNodesFromRect(DocumentOrShadowRoot& aRoot, const nsRect& aRect,
|
|||
return; // return null to premature XUL callers as a reminder to wait
|
||||
}
|
||||
|
||||
aOptions += FrameForPointOption::IgnorePaintSuppression;
|
||||
aOptions += FrameForPointOption::IgnoreCrossDoc;
|
||||
aOptions.mBits += FrameForPointOption::IgnorePaintSuppression;
|
||||
aOptions.mBits += FrameForPointOption::IgnoreCrossDoc;
|
||||
|
||||
AutoTArray<nsIFrame*, 8> frames;
|
||||
nsLayoutUtils::GetFramesForArea({rootFrame, aViewportType}, aRect, frames,
|
||||
|
@ -436,12 +437,12 @@ static void QueryNodesFromRect(DocumentOrShadowRoot& aRoot, const nsRect& aRect,
|
|||
|
||||
template <typename NodeOrElement>
|
||||
static void QueryNodesFromPoint(DocumentOrShadowRoot& aRoot, float aX, float aY,
|
||||
EnumSet<FrameForPointOption> aOptions,
|
||||
FrameForPointOptions aOptions,
|
||||
FlushLayout aShouldFlushLayout,
|
||||
Multiple aMultiple, ViewportType aViewportType,
|
||||
nsTArray<RefPtr<NodeOrElement>>& aNodes) {
|
||||
// As per the spec, we return null if either coord is negative.
|
||||
if (!aOptions.contains(FrameForPointOption::IgnoreRootScrollFrame) &&
|
||||
if (!aOptions.mBits.contains(FrameForPointOption::IgnoreRootScrollFrame) &&
|
||||
(aX < 0 || aY < 0)) {
|
||||
return;
|
||||
}
|
||||
|
@ -499,6 +500,7 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
|
|||
float aLeftSize,
|
||||
bool aIgnoreRootScrollFrame,
|
||||
bool aFlushLayout, bool aOnlyVisible,
|
||||
float aVisibleThreshold,
|
||||
nsTArray<RefPtr<nsINode>>& aReturn) {
|
||||
// Following the same behavior of elementFromPoint,
|
||||
// we don't return anything if either coord is negative
|
||||
|
@ -513,12 +515,13 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
|
|||
|
||||
nsRect rect(x, y, w, h);
|
||||
|
||||
EnumSet<FrameForPointOption> options;
|
||||
FrameForPointOptions options;
|
||||
if (aIgnoreRootScrollFrame) {
|
||||
options += FrameForPointOption::IgnoreRootScrollFrame;
|
||||
options.mBits += FrameForPointOption::IgnoreRootScrollFrame;
|
||||
}
|
||||
if (aOnlyVisible) {
|
||||
options += FrameForPointOption::OnlyVisible;
|
||||
options.mBits += FrameForPointOption::OnlyVisible;
|
||||
options.mVisibleThreshold = aVisibleThreshold;
|
||||
}
|
||||
|
||||
auto flush = aFlushLayout ? FlushLayout::Yes : FlushLayout::No;
|
||||
|
|
|
@ -143,7 +143,8 @@ class DocumentOrShadowRoot {
|
|||
void NodesFromRect(float aX, float aY, float aTopSize, float aRightSize,
|
||||
float aBottomSize, float aLeftSize,
|
||||
bool aIgnoreRootScrollFrame, bool aFlushLayout,
|
||||
bool aOnlyVisible, nsTArray<RefPtr<nsINode>>&);
|
||||
bool aOnlyVisible, float aVisibleThreshold,
|
||||
nsTArray<RefPtr<nsINode>>&);
|
||||
|
||||
/**
|
||||
* This gets fired when the element that an id refers to changes.
|
||||
|
|
|
@ -1238,15 +1238,23 @@ nsDOMWindowUtils::NodesFromRect(float aX, float aY, float aTopSize,
|
|||
float aRightSize, float aBottomSize,
|
||||
float aLeftSize, bool aIgnoreRootScrollFrame,
|
||||
bool aFlushLayout, bool aOnlyVisible,
|
||||
float aVisibleThreshold,
|
||||
nsINodeList** aReturn) {
|
||||
RefPtr<Document> doc = GetDocument();
|
||||
NS_ENSURE_STATE(doc);
|
||||
|
||||
auto list = MakeRefPtr<nsSimpleContentList>(doc);
|
||||
|
||||
// The visible threshold was omitted or given a zero value (which makes no
|
||||
// sense), so give a reasonable default.
|
||||
if (aVisibleThreshold == 0.0f) {
|
||||
aVisibleThreshold = 1.0f;
|
||||
}
|
||||
|
||||
AutoTArray<RefPtr<nsINode>, 8> nodes;
|
||||
doc->NodesFromRect(aX, aY, aTopSize, aRightSize, aBottomSize, aLeftSize,
|
||||
aIgnoreRootScrollFrame, aFlushLayout, aOnlyVisible, nodes);
|
||||
aIgnoreRootScrollFrame, aFlushLayout, aOnlyVisible,
|
||||
aVisibleThreshold, nodes);
|
||||
list->SetCapacity(nodes.Length());
|
||||
for (auto& node : nodes) {
|
||||
list->AppendElement(node->AsContent());
|
||||
|
|
|
@ -783,6 +783,11 @@ interface nsIDOMWindowUtils : nsISupports {
|
|||
* @param aFlushLayout flushes layout if true. Otherwise, no flush occurs.
|
||||
* @param aOnlyVisible Set to true if you only want nodes that pass a visibility
|
||||
* hit test.
|
||||
* @param aTransparencyThreshold Only has an effect if aOnlyVisible is true.
|
||||
* Returns what amount of transparency is considered "opaque enough"
|
||||
* to consider elements "not visible". The default is effectively "1"
|
||||
* (so, only opaque elements will stop an element from being
|
||||
* "visible").
|
||||
*/
|
||||
NodeList nodesFromRect(in float aX,
|
||||
in float aY,
|
||||
|
@ -792,7 +797,8 @@ interface nsIDOMWindowUtils : nsISupports {
|
|||
in float aLeftSize,
|
||||
in boolean aIgnoreRootScrollFrame,
|
||||
in boolean aFlushLayout,
|
||||
in boolean aOnlyVisible);
|
||||
in boolean aOnlyVisible,
|
||||
[optional] in float aTransparencyThreshold);
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
|
||||
let dwu = window.windowUtils;
|
||||
|
||||
function check(x, y, top, right, bottom, left, onlyVisible, list, aListIsComplete) {
|
||||
let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, true, false, onlyVisible);
|
||||
function check(x, y, top, right, bottom, left, onlyVisible, list, aListIsComplete = false, aOpacityThreshold = 1.0) {
|
||||
let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, /* aIgnoreRootScrollFrame = */ true, /* aFlushLayout = */ true, onlyVisible, aOpacityThreshold);
|
||||
|
||||
if (!aListIsComplete) {
|
||||
list.push(e.body);
|
||||
|
@ -118,7 +118,23 @@
|
|||
|
||||
let [x, y] = getCenterFor(container);
|
||||
check(x, y, 1, 1, 1, 1, false, [fg, bg, container]);
|
||||
check(x, y, 1, 1, 1, 1, true, [fg], true);
|
||||
|
||||
const kListIsComplete = true;
|
||||
|
||||
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete);
|
||||
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete, 0.5);
|
||||
|
||||
// Occluded with different opacity thresholds, with background colors and
|
||||
// opacity.
|
||||
fg.style.backgroundColor = "rgba(0, 255, 0, 0.5)";
|
||||
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete, 0.4);
|
||||
check(x, y, 1, 1, 1, 1, true, [fg, bg], kListIsComplete, 0.6);
|
||||
|
||||
fg.style.backgroundColor = "";
|
||||
fg.style.opacity = "0.5";
|
||||
|
||||
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete, 0.4);
|
||||
check(x, y, 1, 1, 1, 1, true, [fg, bg], kListIsComplete, 0.6);
|
||||
}
|
||||
|
||||
done();
|
||||
|
|
|
@ -42,7 +42,7 @@ static already_AddRefed<dom::Element> ElementFromPoint(
|
|||
}
|
||||
nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(
|
||||
RelativeTo{rootFrame, ViewportType::Visual}, CSSPoint::ToAppUnits(aPoint),
|
||||
{FrameForPointOption::IgnorePaintSuppression});
|
||||
{{FrameForPointOption::IgnorePaintSuppression}});
|
||||
while (frame && (!frame->GetContent() ||
|
||||
frame->GetContent()->IsInNativeAnonymousSubtree())) {
|
||||
frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame);
|
||||
|
|
|
@ -2641,8 +2641,7 @@ struct AutoNestedPaintCount {
|
|||
#endif
|
||||
|
||||
nsIFrame* nsLayoutUtils::GetFrameForPoint(
|
||||
RelativeTo aRelativeTo, nsPoint aPt,
|
||||
EnumSet<FrameForPointOption> aOptions) {
|
||||
RelativeTo aRelativeTo, nsPoint aPt, const FrameForPointOptions& aOptions) {
|
||||
AUTO_PROFILER_LABEL("nsLayoutUtils::GetFrameForPoint", LAYOUT);
|
||||
|
||||
nsresult rv;
|
||||
|
@ -2653,9 +2652,10 @@ nsIFrame* nsLayoutUtils::GetFrameForPoint(
|
|||
return outFrames.Length() ? outFrames.ElementAt(0) : nullptr;
|
||||
}
|
||||
|
||||
nsresult nsLayoutUtils::GetFramesForArea(
|
||||
RelativeTo aRelativeTo, const nsRect& aRect,
|
||||
nsTArray<nsIFrame*>& aOutFrames, EnumSet<FrameForPointOption> aOptions) {
|
||||
nsresult nsLayoutUtils::GetFramesForArea(RelativeTo aRelativeTo,
|
||||
const nsRect& aRect,
|
||||
nsTArray<nsIFrame*>& aOutFrames,
|
||||
const FrameForPointOptions& aOptions) {
|
||||
AUTO_PROFILER_LABEL("nsLayoutUtils::GetFramesForArea", LAYOUT);
|
||||
|
||||
nsIFrame* frame = const_cast<nsIFrame*>(aRelativeTo.mFrame);
|
||||
|
@ -2665,10 +2665,10 @@ nsresult nsLayoutUtils::GetFramesForArea(
|
|||
builder.BeginFrame();
|
||||
nsDisplayList list;
|
||||
|
||||
if (aOptions.contains(FrameForPointOption::IgnorePaintSuppression)) {
|
||||
if (aOptions.mBits.contains(FrameForPointOption::IgnorePaintSuppression)) {
|
||||
builder.IgnorePaintSuppression();
|
||||
}
|
||||
if (aOptions.contains(FrameForPointOption::IgnoreRootScrollFrame)) {
|
||||
if (aOptions.mBits.contains(FrameForPointOption::IgnoreRootScrollFrame)) {
|
||||
nsIFrame* rootScrollFrame = frame->PresShell()->GetRootScrollFrame();
|
||||
if (rootScrollFrame) {
|
||||
builder.SetIgnoreScrollFrame(rootScrollFrame);
|
||||
|
@ -2677,12 +2677,13 @@ nsresult nsLayoutUtils::GetFramesForArea(
|
|||
if (aRelativeTo.mViewportType == ViewportType::Layout) {
|
||||
builder.SetIsRelativeToLayoutViewport();
|
||||
}
|
||||
if (aOptions.contains(FrameForPointOption::IgnoreCrossDoc)) {
|
||||
if (aOptions.mBits.contains(FrameForPointOption::IgnoreCrossDoc)) {
|
||||
builder.SetDescendIntoSubdocuments(false);
|
||||
}
|
||||
|
||||
builder.SetHitTestIsForVisibility(
|
||||
aOptions.contains(FrameForPointOption::OnlyVisible));
|
||||
if (aOptions.mBits.contains(FrameForPointOption::OnlyVisible)) {
|
||||
builder.SetHitTestIsForVisibility(aOptions.mVisibleThreshold);
|
||||
}
|
||||
|
||||
builder.EnterPresShell(frame);
|
||||
|
||||
|
|
|
@ -747,16 +747,32 @@ class nsLayoutUtils {
|
|||
OnlyVisible,
|
||||
};
|
||||
|
||||
struct FrameForPointOptions {
|
||||
using Bits = mozilla::EnumSet<FrameForPointOption>;
|
||||
|
||||
Bits mBits;
|
||||
// If mBits contains OnlyVisible, what is the opacity threshold which we
|
||||
// consider "opaque enough" to clobber stuff underneath.
|
||||
float mVisibleThreshold;
|
||||
|
||||
FrameForPointOptions(Bits aBits, float aVisibleThreshold)
|
||||
: mBits(aBits), mVisibleThreshold(aVisibleThreshold){};
|
||||
|
||||
MOZ_IMPLICIT FrameForPointOptions(Bits aBits)
|
||||
: FrameForPointOptions(aBits, 1.0f) {}
|
||||
|
||||
FrameForPointOptions() : FrameForPointOptions(Bits()){};
|
||||
};
|
||||
|
||||
/**
|
||||
* Given aFrame, the root frame of a stacking context, find its descendant
|
||||
* frame under the point aPt that receives a mouse event at that location,
|
||||
* or nullptr if there is no such frame.
|
||||
* @param aPt the point, relative to the frame origin, in either visual
|
||||
* or layout coordinates depending on aRelativeTo.mViewportType
|
||||
* @param aFlags some combination of FrameForPointOption.
|
||||
*/
|
||||
static nsIFrame* GetFrameForPoint(RelativeTo aRelativeTo, nsPoint aPt,
|
||||
mozilla::EnumSet<FrameForPointOption> = {});
|
||||
const FrameForPointOptions& = {});
|
||||
|
||||
/**
|
||||
* Given aFrame, the root frame of a stacking context, find all descendant
|
||||
|
@ -765,11 +781,10 @@ class nsLayoutUtils {
|
|||
* @param aRect the rect, relative to the frame origin, in either visual
|
||||
* or layout coordinates depending on aRelativeTo.mViewportType
|
||||
* @param aOutFrames an array to add all the frames found
|
||||
* @param aFlags some combination of FrameForPointOption.
|
||||
*/
|
||||
static nsresult GetFramesForArea(RelativeTo aRelativeTo, const nsRect& aRect,
|
||||
nsTArray<nsIFrame*>& aOutFrames,
|
||||
mozilla::EnumSet<FrameForPointOption> = {});
|
||||
const FrameForPointOptions& = {});
|
||||
|
||||
/**
|
||||
* Transform aRect relative to aFrame up to the coordinate system of
|
||||
|
|
|
@ -608,7 +608,6 @@ nsDisplayListBuilder::nsDisplayListBuilder(nsIFrame* aReferenceFrame,
|
|||
mForceLayerForScrollParent(false),
|
||||
mAsyncPanZoomEnabled(nsLayoutUtils::AsyncPanZoomEnabled(aReferenceFrame)),
|
||||
mBuildingInvisibleItems(false),
|
||||
mHitTestIsForVisibility(false),
|
||||
mIsBuilding(false),
|
||||
mInInvalidSubtree(false),
|
||||
mDisablePartialUpdates(false),
|
||||
|
@ -2810,9 +2809,37 @@ void nsDisplayList::HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect,
|
|||
}
|
||||
|
||||
if (aBuilder->HitTestIsForVisibility()) {
|
||||
if (aState->mHitFullyOpaqueItem ||
|
||||
item->GetOpaqueRegion(aBuilder, &snap).Contains(aRect)) {
|
||||
aState->mHitFullyOpaqueItem = true;
|
||||
aState->mHitOccludingItem = [&] {
|
||||
if (aState->mHitOccludingItem) {
|
||||
// We already hit something before.
|
||||
return true;
|
||||
}
|
||||
if (aState->mCurrentOpacity == 1.0f &&
|
||||
item->GetOpaqueRegion(aBuilder, &snap).Contains(aRect)) {
|
||||
// An opaque item always occludes everything. Note that we need to
|
||||
// check wrapping opacity and such as well.
|
||||
return true;
|
||||
}
|
||||
float threshold = aBuilder->VisibilityThreshold();
|
||||
if (threshold == 1.0f) {
|
||||
return false;
|
||||
}
|
||||
float itemOpacity = [&] {
|
||||
switch (item->GetType()) {
|
||||
case DisplayItemType::TYPE_OPACITY:
|
||||
return static_cast<nsDisplayOpacity*>(item)->GetOpacity();
|
||||
case DisplayItemType::TYPE_BACKGROUND_COLOR:
|
||||
return static_cast<nsDisplayBackgroundColor*>(item)
|
||||
->GetOpacity();
|
||||
default:
|
||||
// Be conservative and assume it won't occlude other items.
|
||||
return 0.0f;
|
||||
}
|
||||
}();
|
||||
return itemOpacity * aState->mCurrentOpacity >= threshold;
|
||||
}();
|
||||
|
||||
if (aState->mHitOccludingItem) {
|
||||
// We're exiting early, so pop the remaining items off the buffer.
|
||||
aState->mItemBuffer.TruncateLength(itemBufferStart);
|
||||
break;
|
||||
|
@ -5777,6 +5804,9 @@ void nsDisplayOpacity::HitTest(nsDisplayListBuilder* aBuilder,
|
|||
const nsRect& aRect,
|
||||
nsDisplayItem::HitTestState* aState,
|
||||
nsTArray<nsIFrame*>* aOutFrames) {
|
||||
AutoRestore<float> opacity(aState->mCurrentOpacity);
|
||||
aState->mCurrentOpacity *= mOpacity;
|
||||
|
||||
// TODO(emilio): special-casing zero is a bit arbitrary... Maybe we should
|
||||
// only consider fully opaque items? Or make this configurable somehow?
|
||||
if (aBuilder->HitTestIsForVisibility() && mOpacity == 0.0f) {
|
||||
|
|
|
@ -1769,10 +1769,15 @@ class nsDisplayListBuilder {
|
|||
AnimatedGeometryRoot* AnimatedGeometryRootForASR(
|
||||
const ActiveScrolledRoot* aASR);
|
||||
|
||||
bool HitTestIsForVisibility() const { return mHitTestIsForVisibility; }
|
||||
bool HitTestIsForVisibility() const { return mVisibleThreshold.isSome(); }
|
||||
|
||||
void SetHitTestIsForVisibility(bool aHitTestIsForVisibility) {
|
||||
mHitTestIsForVisibility = aHitTestIsForVisibility;
|
||||
float VisibilityThreshold() const {
|
||||
MOZ_DIAGNOSTIC_ASSERT(HitTestIsForVisibility());
|
||||
return mVisibleThreshold.valueOr(1.0f);
|
||||
}
|
||||
|
||||
void SetHitTestIsForVisibility(float aVisibleThreshold) {
|
||||
mVisibleThreshold = mozilla::Some(aVisibleThreshold);
|
||||
}
|
||||
|
||||
bool ShouldBuildAsyncZoomContainer() const {
|
||||
|
@ -2054,7 +2059,6 @@ class nsDisplayListBuilder {
|
|||
bool mForceLayerForScrollParent;
|
||||
bool mAsyncPanZoomEnabled;
|
||||
bool mBuildingInvisibleItems;
|
||||
bool mHitTestIsForVisibility;
|
||||
bool mIsBuilding;
|
||||
bool mInInvalidSubtree;
|
||||
bool mBuildCompositorHitTestInfo;
|
||||
|
@ -2066,6 +2070,7 @@ class nsDisplayListBuilder {
|
|||
bool mIsRelativeToLayoutViewport;
|
||||
bool mUseOverlayScrollbars;
|
||||
|
||||
mozilla::Maybe<float> mVisibleThreshold;
|
||||
nsRect mHitTestArea;
|
||||
CompositorHitTestInfo mHitTestInfo;
|
||||
};
|
||||
|
@ -2607,10 +2612,13 @@ class nsDisplayItem : public nsDisplayItemBase {
|
|||
|
||||
// Handling transform items for preserve 3D frames.
|
||||
bool mInPreserves3D = false;
|
||||
// When hit-testing for visibility, we may hit a fully opaque item in a
|
||||
// When hit-testing for visibility, we may hit an fully opaque item in a
|
||||
// nested display list. We want to stop at that point, without looking
|
||||
// further on other items.
|
||||
bool mHitFullyOpaqueItem = false;
|
||||
bool mHitOccludingItem = false;
|
||||
|
||||
float mCurrentOpacity = 1.0f;
|
||||
|
||||
AutoTArray<nsDisplayItem*, 100> mItemBuffer;
|
||||
};
|
||||
|
||||
|
@ -4997,6 +5005,8 @@ class nsDisplayBackgroundColor : public nsPaintedDisplayItem {
|
|||
|
||||
bool CanApplyOpacity() const override;
|
||||
|
||||
float GetOpacity() const { return mColor.a; }
|
||||
|
||||
nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override {
|
||||
*aSnap = true;
|
||||
return mBackgroundRect;
|
||||
|
|
|
@ -1174,9 +1174,9 @@ bool nsTypeAheadFind::IsRangeRendered(nsRange* aRange) {
|
|||
// Append visible frames to frames array.
|
||||
nsLayoutUtils::GetFramesForArea(
|
||||
RelativeTo{rootFrame}, r, frames,
|
||||
{FrameForPointOption::IgnorePaintSuppression,
|
||||
FrameForPointOption::IgnoreRootScrollFrame,
|
||||
FrameForPointOption::OnlyVisible});
|
||||
{{FrameForPointOption::IgnorePaintSuppression,
|
||||
FrameForPointOption::IgnoreRootScrollFrame,
|
||||
FrameForPointOption::OnlyVisible}});
|
||||
|
||||
// See if any of the frames contain the content. If they do, then the range
|
||||
// is visible. We search for the content rather than the original frame,
|
||||
|
|
Загрузка…
Ссылка в новой задаче